# Course Outline

Syllabus and Learning Outcomes

## **Syllabus**

**Week 1:** Intro and Thinking Like a Programmer\
**Week 2:** Programming Languages and Python Basics\
**Week 3:** Types, Lists and Dictionaries\
**Week 4:** While loops and For loops\
**Week 5:** Functions, and import\
**Week 6:** File I/O and String Manipulation\
**Week 7:** Datascience Pt. I - Numpy, Fitting and Prediction\
**Week 8:** Datascience Pt. II - Matplotlib, Data Visualization\
**-- Easter Break --**\
**Week 9:** Introductory machine learning with SciKit learn\
**Week 10:** Programming in the *Real World*

## Learning Outcomes

Students with little to no prior exposure to programming or python should feel comfortable with all concept covered in the class, and be able to apply the techniques discussed to problems they face in university courses or research.

Once equipped with a foundational understanding of programming, students will be able to independently build upon this by learning new languages, or increasing the sophistication of their python knowledge.

## Acknowledgement

Many of the topics and exercises are heavily inspired by other courses which teach introductory programming and/or python, and we have made no effort to thoroughly cite sources.

However, Harvard's CS50 (<https://cs50.harvard.edu/college/>) was the source of much inspiration for the early parts of the course, and for some of the exercises.


# Course Organization

Organizational Information

## Weekly Lectures

**When:** 5-6pm on Tuesdays\
**Where:** Collab 1 in the Kilburn building\
**Format:** The first 15-20min will consist of a short presentation given by Alex and Luke, followed by 40min of working on exercises, with the assistance of TAs

## Facebook Events

Recurring Facebook events: <https://www.facebook.com/events/292795478097170/>


# TAs

TA Contact Details

## Course Leaders

### Alex

Contact: alex (at) afspies.com\
Office Hours: Friday 3:10-4:10pm @Byte Cafe (kilburn)

### Luke


# Week 1

Course Introduction and Thinking Like a Programmer

## 1.1 Introduction

### Why should I care about programming?

#### Completing course work / projects

Many students will find themselves needing to solve problems by programming at some point - be it programming an experimental apparatus using an archaic instruction set, writing a suite of fancy software, or analyzing and predicting trends from some data set.

In all these cases it is important to remember that programming, like most technical skills, is something which gets easier with time and practice - Even if you never use python again, you'll find it much easier to learn a new programming language once you've spent hours getting angry at another one, as they all ultimately rely on the same kinds of thinking.

#### Employability

The ability to code is perhaps the most sought-after technical skill, along side mathematics, in a world where attempts to automate and digitize permeate an increasing number of jobs.

![Compounds growth rates for jobs in 2015-2020 (source: World Economic Forum) ](https://158957078-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-LoobEkeCSQp9d3FJslb%2F-LoobZAAC99Hg9Lu22Zg%2F-LoobaoJ52R58t1SK7FY%2Fimage%20\(7\).png?generation=1568545863762056\&alt=media)

#### Fun!

This is easily the most important reason to learn to code - *coding is fun!*

We live in a digital world, and so having the skills to not just appreciate and understand how software and technology work, but also to manipulate them, or to create your own, unlocks more doors than you imagine.

Maybe you'll create an app which automatically sends messages to your housemates telling them to do the dishes, or you'll code an artificial intelligence which takes over the world - what you do with the skills we aim to provide is up to you.

![A totally accurate depiction of a hacker having fun (source: Kilburn after hours)](https://158957078-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-LoobEkeCSQp9d3FJslb%2F-LoobZAAC99Hg9Lu22Zg%2F-LoobaoLbKGBqoK-V3oX%2Fimage.png?generation=1568545863574303\&alt=media)

### And *you're* going to teach me?

*Alright,* we'll admit that we're not grizzled veterans of pedagogy, and (hopefully) lack the tweed coats and wrinkled skin which wisened lecturers don. However, it wasn't that long ago that we were first getting to grips with programming ourselves (though sometimes it sure feels like it), and we think we've got a pretty good idea of not just how to code, but also how to learn to.

## 1.2 Representing Information

### Binary

Computers store information as binary values, consisting only of **1**s and **0**s. The reasons for this ultimately come down to the fact that is more practical to build electronics which represent only two distinct states, and are thus able to encode binary values.

Binary numbers define a "**base 2**" counting system, where as the one we are used to is "**base 10**" (known as **Decimal**) - For example, in base 10, the number 126 is represented by:

$$
126=(1\times100)+(2\times10)+(6\times1)
$$

Where each column represents the powers of 10; i.e.

$$
126=(1\times10^2)+(2\times10^1)+(6\times10^0)
$$

In binary, numbers are represented similarly, except that the base is now 2 rather than 10. So the number 13 would be written as

$$
13 = (1\times2^3)+(1\times2^2)+(0\times2^1)+(1\times2^0)
$$

Which means that the binary representation of 13 in binary is simply 1101, where each column represents the coefficient for the powers of 2, similar to the way things work in base 10.

This may seem abstract or unnecessary, but the point of showing you binary is simply to prove to you that it is possible to represent any number using only 1s and 0s. Combined with the fact that **we can represent anything in terms of numbers**, this means computers can stores all kinds of information using only 1s and 0s at the electronic level.

Luckily for us, most programmers rarely work at such a low "**machine-level**", so don't worry about the details!

### ASCII

A key example of how we can represent every-day information in terms of numbers is provided by the ASCII character encoding standard. The ASCII standard defines a **mapping** from numbers to characters, including not just the alphabet, but also many common symbols like &, # !, etc.

The ASCII standard allows us to convert English words to numbers, which can in turn be stored and manipulated by computers, using their binary representation. For example, the word "Hello":

| Alphabetical | ASCII              | Binary                                  |
| ------------ | ------------------ | --------------------------------------- |
| Hello        | 72 101 108 108 111 | 1001000 1100101 1101100 1101100 1101111 |

### RGB

There are many kinds of information we might want to represent on our computers besides **strings of characters** (i.e. words and sentences). One obvious example is images, which ultimately consist of many thousands of individual "pixels" which have a specific color.

One simple way to encode colors is to decompose them into three primary colors, typically: **Red, Green and Blue**, and to figure out how much of each color you'd have to "mix" together to get the desired color (known as additive mixing).

The RBG standard encodes this information using three numbers between 0-255 which represent the "amount" of each color present in the decomposition. For example:

![An example of an RGB decomposition for a specific shade of pink ](https://158957078-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-LoobEkeCSQp9d3FJslb%2F-LoobZAAC99Hg9Lu22Zg%2F-LoobaoNumPIj02lsRXd%2Fimage%20\(12\).png?generation=1568545863284142\&alt=media)

Some of you may be old enough to remember back in the good old days before HDMI, when SCART cables were still commonly used - these actually had red, green and blue signal channels. Furthermore, pixels on most modern displays actually consist of distinct Red, Green and Blue pixels, each of which changes brightness to give the appearance of millions of different colors.

![Left: SCART cable - these had actual RGB channels | Right: TV up close - pixels consist of RGB](https://158957078-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-LoobEkeCSQp9d3FJslb%2F-LoobZAAC99Hg9Lu22Zg%2F-LoobaoPEvIVCqxFtr9a%2Fimage%20\(4\).png?generation=1568545863500019\&alt=media)

## 1.3 Abstraction

**Abstraction** is a concept in computer science where some low-level implementation (such as how data is ultimately stored in binary) is simplified or taken for granted, so we can use that implementation at a higher level (such as representing letters, that we can then use in our programs).

### Emojis

Emojis provide a great example of abstraction - let's take a favorite of mine, the "[pile of poo](https://www.iemoji.com/view/emoji/55/smileys-people/pile-of-poo)" emoji:💩.

Similar to the way in which common characters are encoded in the ASCII scheme, a more complex standard known as Unicode is used to represent other objects typically sent in messages - including emojis. For example, in the UTF-8 standard, the 💩 is given by "U+14F4A9".

Computers compatible with this encoding standard will know that it corresponds to a given set of RGB values, describing a 256x256 pixel image:

![You say "pile of poo", I say 256x256 RGB values](https://158957078-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-LoobEkeCSQp9d3FJslb%2F-LoobZAAC99Hg9Lu22Zg%2F-LoobaoROokI40H_pqde%2Fimage%20\(1\).png?generation=1568545863489439\&alt=media)

This is an excellent example of abstraction, because rather than sending your friends a set of 65536 RGB vectors, you just send them ":poop-emoji:" and rely on the fact that someone else has taught your computer to convert that to an image of a smiling piece of feces.

|   |
| - |

![Algorithms take the inputs specified by a problem and output the solution](https://158957078-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-LoobEkeCSQp9d3FJslb%2F-LoobZAAC99Hg9Lu22Zg%2F-LoobaoTsJ2__Hwlc0b2%2Fimage%20\(17\).png?generation=1568545863363668\&alt=media)

Without going into any detail here, it is important to note that the essence of what many programmers do is **problem solving** - more specifically, designing **algorithms** which take the inputs defining a problem (in a specified representation) and output the solution (in a specified representation).

For example, I might develop an algorithm which tells me what day it will be tomorrow, based on what day it currently is. This would work something like:

```
If today is Monday => Tomorrow is Tuesday
If today is Tuesday => Tomorrow is Wednesday
...
If today is Sunday => Tomorrow is Monday
```

Where we have chosen a representation in which the days of the week are given by their names in English. Instead, we might be producing an algorithm for a group of efficient Germans, who tell us the day in German, and want the output as a number between 1 and 7 (to save valuable syllables from being spoken):

```
If today is Montag => Tomorrow is 2
If today is Dienstag => Tomorrow is 3
...
If today is Sonntag => Tomorrow is 1
```

**Note**: This highlights an important detail about creating algorithms - Although in both these cases the algorithm works in essentially the same way, the data we feed to it and the data we want it to give us are different, and so the algorithms are distinct. In essence:

$$
Algorithm = Some \ Function + Form \ of \ Data
$$

## 1.5 Pseudocode

### Making a jam sandwich

Suppose you're trying to tell a robot how to go about making a jam sandwich, you might start by giving it instructions along the lines of:

```
Get the jam
Put two pieces of bread on a plate
Put jam on the bread
Put the pieces of bread on top of eachother
```

And that would probably be more than sufficient, if not slightly patronizing, if you were talking to a competent human. However, what we'll be learning is how to tell computers what to do, and computers are *truly* *incompetent* - no common sense whatsoever!

So let's try again, and be really explicit this time:

```
Get jam jar from cupboard
Place jam jar on kitchen counter and unscrew lid
Take a knife from the draw
...
```

This is better, but still not even close to specific enough - computers need to be told *exactly* what they need to do at (preferably) the highest-level which they can understand (which is very low, by human standards). So let's suppose our robot has some sensors which can measure distance, and that it knows about angles and movement.

Then we can start by opening the cupboard which contains the jam:

```
Using left hand, peform <grip> on handle at x,y,z = (17cm, 21cm, 80cm)
Move left hand in a 60 degree arc in the x-y plane about x,y = (8cm, 21cm)
Using left hand, perform <release>
```

Ok, we did it, we managed to tell the robot how to open the cupboard - Yes... I can hear you saying "Maybe I'll just get a subway instead", but don't worry, we can use **abstraction** to make our lives easier.

Notice that in the example above the robot understands an instruction called \<grip>, presumably because the manufacturer thought it would come in handy. We can also create our own complicated instructions by abstracting away whole sets of consecutive moves into so-called **functions,** which we've been denoting using <>.

For example, suppose we went through the horrible effort of teaching the robot how to google an object's name, recognize it and then move it to another object, referring to this instruction as **\<pick up x and place at y>,** then we could avoid bothering about coordinates and angles entirely, except for the one time where we define the function:

```
function <pick up x and place at y>
    objectImage = <google image search x>
    objectCoordinate = <find closest matching obect to objectImage>
    Using left hand, perform <grip> on objectCoordinate
    ...
```

If we repeated this process of building layer upon layer of abstraction, then we might eventually end up with a single function called **\<make jam sandwich>,** which we could now tell the robot to carry out as many times as we liked (though it's not clear what happens when we run out of jam or bread!)

In much the same way, most programmers rarely have to dig down to the "nitty gritty" to solve real world problems, as they can instead rely on all the **functions** which other people have implemented in **libraries** or **packages,** when solving related problems before - we'll come back to this later.


# Week 2

Programming Languages and Python

## Recap

Last time we discussed how we can use pseudocode to break down complex tasks into manageable chunks, before starting to issue exact instructions. We saw this in action during the jam sandwich demonstration.

And while pseudocode is nice, we ultimately need a consistent way of telling computers what to do - To this end, programming languages have been developed which allow allow programmers to write fairly readable code which is then converted into in machine-level binary code by a compiler (luckily, we don't need to worry about this).

## Python

### Why Python?

**Easy to learn**\
Python reads almost like English, just with a few extra rules thrown in - at least when compared to some other popular programming languages!

**Fairly fast**\
As a rule of thumb, one can usually assume that the more readable a language is, the slower and less customization it is. However, python manages can still be very fast when used sensibly, especially for routine data science and machine learning tasks.

**Popular**\
Python's popularity means that many libraries have been written for it - these allow us to do everything from data science through to interacting with spotify and twitter.

Additionally, python is an increasingly popular language in industry. It is used extensively for data science and machine learning work (especially at Google) as well as for prototyping new technologies (because it's quick and easy to get stuff working in it).

### How Python?

For the purposes of this course we'll be using the **Anaconda** python distribution, with python version 3.7

This is because the Anaconda distribution comes with many packages pre-installed which will come in handy later, and because it comes with the **Spyder** IDE (integrated-development-environment), which makes writing and running python code a breeze.

## Python - Baby Steps

### Syntax and Comments

The **syntax** of a language describes the rules and instructions which can be used when coding.

All programming languages have specific ways of including normal English inside your **scripts** (i.e. files filled with code) which can be used to explain the function of the code. These are referred to as **comments,** because they allow us to comment on the code.

In python, single-line comments are started with hashtag (#) symbols and cause following text on the same line to be ignored by python. This allows the programmer to add explanations / notes to their code such that other programmers can understand it more easily. (Comments also serve as useful reminders for the programmers themselves)

Additionally, and text enclosed between a pair of triple-double (""") quotes forms a comment (which can be multi-line):

```python
# Hashtags denote one-line comments - These are ignored by python
# Use comments to explain what the code is doing

""" Triple quotes can be used for multi-line comments
    These are usually used for file headers or when declaring fuctions 
"""
```

### Hello World

In Python, the **print** statement allows us to output text to the console:

{% code title="hello\_world.py" %}

```python
# Say "Hello World!" to the user, via the terminal, when the code runs
print("Hello World!")
```

{% endcode %}

When specifying the **string** which is being printed, we need to be careful to ensure that the enclosing quotes are identical - otherwise python gets confused and doesn't think the string has ended.

{% code title="string\_quotes.py" %}

```python
print('I am a string')
print("So am I!")
print("Hey, can I join you guys?') # This wont work!

# You can have different quotes as part of your string
print("Gnomic as ever, Alex moaned 'Alea Iacta Est'")
```

{% endcode %}

{% hint style="danger" %}

#### Indentation

Other languages have lots of brackets ({\[]}) all over the place - python only cares about **indentation;** I.e. things need to lined up nicely, or indented if they are **nested** in each other:

```python
# Indentation needs to be consistent
print("Hello World!")
    print("Python won't run this!")
```

{% endhint %}

**Variables**

Variables are what we use to store information within our code, and can be thought of as labelled boxes containing data.

![Variables can be thought of as labelled boxes containing data](https://158957078-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-LoobEkeCSQp9d3FJslb%2F-LoobZAAC99Hg9Lu22Zg%2F-LoobaxF7fpbxasEP52a%2Fimage%20\(15\).png?generation=1568545863525734\&alt=media)

**Declaration**

{% code title="declaration\_example.py" %}

```python
# Declare and assign the variables
my_string = "Hello World"
my_integer = 1239082
my_boolean = True

# Reassign a variable to different type
my_boolean = 47
```

{% endcode %}

**pascal case vs camelcase**

{% code title="pascal\_vs\_camel.py" %}

```python
# Camel case and pascal case are the two most common variable naming schemes
# They don't affect how your code runs, but "code readability" is important!

aCamelCaseVariable = "This is not the python way"
a_pascal_case_variable = "This is the python way"
```

{% endcode %}

### Maths

| Operator | Description    | Example  | Output |
| -------- | -------------- | -------- | ------ |
| +        | Addition       | 17 + 3   | 20     |
| -        | Subtraction    | 14 - 21  | -7     |
| \*       | Multiplication | 3 \* 24  | 72     |
| /        | Division       | 10 / 4   | 2.5    |
| \*\*     | Exponentiation | 2 \*\* 4 | 16     |
| %        | Modulo         | 22 % 3   | 1      |

{% hint style="warning" %}
In the case of division, the output has a decimal point even though the operands are both integers - python is nice like that.

You will often need to be careful to not mix decimal and integer values when performing certain operations, as you may incur rounding error (we will discuss this more later on when we cover data types, and the difference between "int"s and "float"s).
{% endhint %}

### Boolean Logic and Conditions

Mathematical logic essentially allows us to compare things in a well-defined, consistent way. The ability to do this is essential for allowing us to build useful computer programmes. The types of comparison operations that we can perform in python are outlines in the following table:

| Operator | Description              | Example               | Output |
| -------- | ------------------------ | --------------------- | ------ |
| <        | less than                | 5 < 8                 | False  |
| >        | greater than             | 4.3 > 2               | True   |
| ==       | equals                   | "Hello" == "there"    | False  |
| <=       | less than or equal to    | 5 <= 5                | True   |
| >=       | greater than or equal to | 8 >= 110              | False  |
| !=       | not equal                | "General" != "Kenobi" | True   |

For example

{% code title="logic\_example.py" %}

```python
# An example of using logic

is_this_true = 3 > 10 # Let us see if 3 is > 10; The result should be False
print(is_this_true) # Print out the result, hopefully it will say False
```

{% endcode %}

### If and Else

If statements execute code provided that the specified condition is satisfied

```python
if (True):
    print("Condition was met")
else:
    print("Condition wasn't met")
```

We can use conditional operators to determine when an if statement should trigger.

```python
if (7 > 4):
    print("Computer is clever")
else:
    print("I thought 'it just works' steve?!")
```

Coupled with variables, this allows us to **control** the flow of our code.

#### Else if

In the script above, we used an "if-else" statement, but in some cases we might want to multiple possible specific answers differently. For this we can use "elif" (short for else if) :

{% code title="elif\_example.py" %}

```python
user_name = "Augustus" # We don't need humans!

if (user_name == "Alex"):
    print("I've been expecting you...")
elif (user_name == "Augustus"):
    print("Ad omnes Ave Caesar")
else:
    print("Nice to meet you " + user_name)
```

{% endcode %}

### Input

Sometimes we want the user to give us some information, which we then use somehow in our code. Luckily, python has a built in *function* (aka method) which easily allows us to do this:

```python
# Input returns a string
user_input = input()
```

Now we are ready to create programs which respond differently based on the user's input

{% code title="if\_else\_example.py" %}

```python
user_answer = input("What is 7 + 3? ")

if (user_answer == "10"):
    print("Check you out!") 
else: # Catch all inputs which aren't 10
    print("Happens to the best of us") # Lie to the user
```

{% endcode %}

Finally, it is worth noting that we can concatenate string by using the + operator:

{% code title="input\_example.py" %}

```python
# This code will compliment our human user

name_of_user = input("Hi! What is your name? ") # Get user input

output_string = "Nice to meet you " + name_of_user + " - Looking good! ;)"
print(output_string) # Print our compliment string
```

{% endcode %}


# Extensions

Basic String Manipulation & and/or

## More on Modulo

The modulo operator provides a useful way of checking whether something a multiple of something else - it essentially returns the remainder after as many steps of division as possible:

{% code title="modulu\_example.py" %}

```python
test1 = 12 % 2 # this will equal 0 as 2 divides 12 exactly
test2 = 13 % 2 # this will return 1 as 13 = (6 * 2) + 1

# We can use this to check if something is even or odd!
number = 10
if (number % 2) == 0:
    print("Number divisible by 2. That means it's even!")
else:
    print("How odd ¯\_(ツ)_/¯")
```

{% endcode %}

## More Logic with *and* & *or*

The logical **and** & **or** operators allow us to increase the complexity of logic in our programmes, without needing to a bunch of **nested** (one inside another) if statements. These can be used at declaration, or in our logical conditions, just as with the previously encountered operands (==, != etc.)

* **and** is true when both following statements are true
* **or** is true when at least one of the following statements is true

{% code title="and\_or\_example.py" %}

```python
# Use in declaration
try_and1 = (7 > 6) and ("hi" != "bye") # will be true
try_and2 = (6 > 7) and ("hi" != "bye") # will be false
try_or = (6 < 7) or ("hi" != "bye") # will be true!

# Use in comparison
is_scary = False
has_scales = True

if has_scales and is_scary:
    print("GODZILLA!!! \0/ \0/ \0/")
elif has_scales or is_scary: # one or the other, as "and" wasn't true
    print("Get it away from me!")
else:
    print("How boring...")
```

{% endcode %}

## Sub-component checking with *in*

**in** allows us to check if a substring or item is contained in a specified object. This will be useful when we discuss lists shortly, but we've already seen how this can be used:

{% code title="in\_examply.py" %}

```python
# For strings
if ("a" in "alex") or ("b" in "bobby"):
    print("I can spell!")
else:
    print("Weird alphabet..")
```

{% endcode %}

String manipulation &

Here we see an example of how we can **nest** control statement&#x73;**,** and use the **.lower()** function to ensure our if statements are not sensitive to the capitalization of user input.

{% code title="string\_manip.py" %}

```python
fav_num = 17

my_string = "SoMeMeSS"
my_string = my_string.lower() # Make it lowercase for easier comparison

if "mess" in my_string: # use *in* 
    print("My life")
elif my_string == "doggo": # else-if
    if fav_num == 17: # so-called "nested" if statement
        print("Doggos are awesome, and so is the number 17")
else:
    print("I'm running out of ideas :<")
```

{% endcode %}

## Putting it all together

{% code title="advanced\_input\_example.py" %}

```python
""" Robust and more Complicated example 
    Taking user input and check against different kinds of cases
"""

# Get the user's name
user_name = input("What's your name? ")
user_name = user_name.lower() # Make the string lowercase - overwrite

# Do some conditional logic
if user_name in ['alex', 'luke']: # Can check multiple cases in one
    print("Your Majesty...")
elif ("z" in user_name) or ("q" in user_name): # Check multiple substring cases
    print("Shame your name isn't allowed in scrabble!")
else:
    print("Heyo")
```

{% endcode %}


# Week 3

Types, Lists and Dictonaries

## Types

The different kinds (or representations) of data which programming languages are able to store and manipulate are referred to as types, and we have already encountered a few of these.

### Simple Types

The types we've encountered so far can be though of as the building blocks for more complicated types - in the same way you might think of a string as being built out of characters.

These **"simple" types** are shown in the following example:

```python
# Basic Types
a_string = "Hello World"
a_character = "Z" # Characters are strings of length 1
a_integer = 32
a_float = 18.032
a_boolean = True  # Booleans are just integers 1 (True) or 0 (False)
```

Little tip:

```python
another_bool = (242 == "frog") # False
```

**Fun fact:** Booleans are actually implemented as integers, with 1 = True and 0 = False:

```
if 0:
    print("I won't get printed")
else:
    print("I will!")
```

### Typecasting

Python, like many other languages, has the ability to transform one type of data to another - although it will only work if this conversion **makes sense**. For example, you could convert a string containing a number to an integer:

```python
# Suppose the user gave us a number via input()
user_number = "12314"

# Cast the string to an integer and store the result
user_number_converted = int(user_number)
```

But, if you tried to convert a string containing letters or symbols to a number, python will get confused and **throw an error** (see [extension material](/material/week-3/extensions#basic-error-handling) for more detail). Here's an example that results in an error:

```python
# Can only cast to other types if compatible
user_input = "Woops"

# Trying to cast this str -> int will cause a "ValueError"
user_input_converted = int(user_input)
```

For reference, here's a table of casts that can (and will) come in handy:

| Type                  | Cast    | Example Use case                                   |
| --------------------- | ------- | -------------------------------------------------- |
| Integer Number        | int()   | Do maths on user input                             |
| Floating Point Number | float() | Output numbers with decimal places                 |
| String                | str()   | Concatenate numbers with text when printing        |
| Boolean               | bool()  | (Not often in practice - output formatting?)       |
| Characters            | chr()   | Convert integer to ASCII character \[string]       |
| List                  | list()  | Convert tuples / arrays / range() objects to lists |

## Lists

Lists are the first proper "data structure" that we will cover:

* Lists allow us to store multiple values in one object
* Lists can change size without us needing to re-declare them all the time
* Lists can also be used for carrying out vector and matrix arithmetic (though in practice there are libraries like numpy which are far better for such things

Lists are created by using square brackets, \[], and in python they can contain multiple different types of items:

```python
# Create an empty list
my_first_list = []

# Create a list containing some items
my_second_list = [1, 423.32, "Hiya", True]
```

### Accessing list items <a href="#accessing-list-items" id="accessing-list-items"></a>

In python (and many other programming languages) we **count from 0**. So the first item in a list is always at the **"zeroeth" position**. For example:

```python
# Create a list
some_list = [0, 1, 2, 3, "Great Movie", 8, 13]
```

Can be visualized as:

![](https://158957078-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-LoobEkeCSQp9d3FJslb%2F-LoobZAAC99Hg9Lu22Zg%2F-Looba_Os_wiBr6VQCwF%2Fimage%20\(13\).png?generation=1568545863104771\&alt=media)

Then. If we wish to get items from a specific location in our list, we can do this by using square brackets next to the list's name: e.g. **list\[index]**

```python
print(some_list[4]) # Print the fifth element
# outputs: Great Movie
```

A more involved example:

```python
# A great reference
sentence = ["A", "Wizard", "Is","Never","Late","!"]

# Explicity store specific elements in new variables
first_word = sentence[0]
second_word = sentence[1]
fifth_word = sentence[4]

print(first_word + " " + fifth_word + " " + second_word)
# outputs: A Late Wizard
```

### Append

To add items to the end of a list, we use the .append() function:

```python
my_list = []
my_list.append("hello")

# Now my_list = ["hello"]
```

### Pop

We can remove items from a list by using **.pop(index)** to remove the item at the specified index. (**Note:** the function also returns the value that it removes)

```
my_list = [3, 5, 2, 6, 4]
my_list.pop(2)

# Now my_list = [3, 5, 6, 4]
```

### Reverse Iteration

If you want to iterate backwards through a list, the following syntax allows it.

```
# Strings are effectively lists of characters
the_dream = "desserts"
university = the_dream[::-1] # We can go backwards too
```

This syntax is a special case of a general language feature called **list slicing** (see [extensions](/material/week-3/extensions#List-Slicing) for detail).

### List Operations

We can add (concatenate) and multiply lists using the familiar **+** and \* operators:

```python
# Concatenate two lists
list_one = ["One", "Two", "Three"]
list_two = [4, 5, 6]
new_list = list_one + list_two
print(new_list) # ["One", "Two", "Three", 4, 5, 6]
```

```python
# Multiplying a list
my_list = ["All", "Work", "And", "No", "Play",
           "Makes", "Alex", "A", "Dull", "Boy"]
new_list = my_list * 20
print(new_list) # ["All", "Work" .......... x20 ]
```

## Dictionaries

Along with lists, dictionaries are among the most useful and commonly used data structures. Dictionaries allow us to store *Key : Value* pairs.

Whereas lists are created using \[], we create dictionaries using {}

```python
# Create an empty dictionary
empty_dict = {}

# Create a dictionary with some info inside
my_dict = {"name" : "alex", "age" : 22, 1 : "one"}
```

### Adding Elements

To add elements to a dictionary we again use the square bracket syntax, except with a key rather than an index:

```python
empty_dict={}

empty_dict["first key"] = 23123
empty_dict["second key"] = "hello"

# Now empty_dict = {"first key" : 23123, "second key": "hello"}
```

{% hint style="warning" %}
Keys in a dictionary must be **unique**. If a key already exists in the dictionary, then its corresponding value will be overridden when you assign it:

```python
my_dict["hello"] = 2312
my_dict["hello"] = -17

# Now my_dict = {"hello" : -17}
```

{% endhint %}

### Pop

Similar to with lists, we can use .pop(key) on a dictionary to remove a key-value pair; usefully, the function also returns the value which it is removing, so we can print it or store it somewhere else.

```python
my_dict = {"hello" : 231, "fish" : "frog"}

# Remove the key-value pair corresponding to "hello"
my_dict.pop("hello")

# Now my_dict = {"fish" : "frog"}
```

### Keys

They .keys() (member) function can be called on a dictionary to get a **list** of its keys:

```python
my_dict = {"hello" : 231, "fish" : "frog"}

my_keys = my_dict.keys()
# then my_keys = ["hello", "fish"]
```

Used alongside **in** (see [extensions](/material/week-2/week-2-extension#sub-component-checking-with-in)) this provides us with an easy way of checking whether we have already stored a particular key in our dictionary - useful for a username:password database!


# Extensions

List Slicing, Robust typecasting via Error Handling

## Useful Tip: Tab completion in Spyder

When typing out the names of variables which you've declared above, you can **press the tab key** to autocomplete (or show a list of possible completions, from which you can then pick)

In practice, such tricks make writing large pieces of code much faster (tab completion also works with most syntax like print()) - especially if you're using informative, but long, variable names.

## More List Stuff

### List Slicing <a href="#list-slicing" id="list-slicing"></a>

Sometimes we want to access sub-sections of our list, without needing to manually get every item contained in that section. To this end we can use colons when accessing arrays.

* This works like **my\_list\[begin:end:step\_size]**

{% hint style="info" %}
**NOTE:** you don't need to specify all three of these values - python will use default values:

* begin = start of list
* end = end of list
* step\_size = 1
  {% endhint %}

\*\*\*\*

```python
sentence = ["What", "A", "Great", ",", "Guy","Never","Said", "Anything","Bad"]

# Emulate the media
out_of_context = sentence[4:8:1] # Get part of the list
print(out_of_context)
```

### Min and Max <a href="#min-and-max" id="min-and-max"></a>

**min()** and **max()** do what you'd expect - So long as you have numbers in the list; string comparison is a bit less straightforward

```python
my_numbers = [5,-2,71,-438,9]

print(max(my_numbers)) # 71
print(min(my_numbers)) # -438
```

### Sort <a href="#sort" id="sort"></a>

Under the hood, min() and max() work by sorting the list first. We can do this manually by using **.sort()**&#x49;n \[24]:

```python
# Example of sorting
# NOTE: don't mix strings and numbers if you want to sort!
my_list = [4, 7, -20, 32, 9.3] 
my_list.sort() # This sorts the list

print(my_list) # [-20, 4, 7, 9.3, 32]
```

### Split <a href="#split" id="split"></a>

**split()** is a function which allows us to break up strings into sub-strings stored in a list

```python
# Example of split()
my_sentence = "I am too lazy to make sentences into lists"
my_list = my_sentence.split(" ") # Split up string by empty spaces
v
print(my_list)
```

### Join <a href="#join" id="join"></a>

**join()** is essentially the opposite of split, and allows us to nicely format our lists when we want to print them

```python
# Example of join()
# NOTE: all items in the list must be strings - unless you do something clever! ;)
my_list = ["I'm", "12", "now", "mum", "I", "can", "do", "what", "I", "want"]
my_sentence = " ".join(my_list) # Join items in list with an empty space

print(my_sentence)
```

## Basic Error Handling

If we try to something which python can't, then we'll get an error. The jargon for this is that : "python throws an error", and we can "catch" these to stop our programs from crashing.

The syntax for is referred to as a "try, except":

```python
try:
    # do stuff here which might throw an error
    weird_function_does_bad_stuff()
except: # this code runs if an error was thrown above
    print("Oops - I encountered an error!")
```


# Week 4

Control - while and for

## Control

### While

While loops allow us to do something **while** a condition is met - providing the first statement which can drastically *control* the flow of our program. The syntax for these is very similar if statements, with the important distinction that the code inside runs continuously until the condition becomes False:

```python
while condition:
    print("Repeated until condition = false")
```

It's important to note that if the condition never becomes false, then the loop will run forever!

```python
# This will run forever! - "infinite while loop"
while True:
    print ("Shame...")
```

In this case, you can abort with ctrl+c (in the console) or by pressing the stop button in most IDEs.

Here's an example of how we can use a "counter" with a while loop:

```python
number = 0

# Keep looping until number has reached the value of 5
while number < 5:
    number += 1
    print(number)
```

And an example of waiting for a correct user input:

```python
acceptance = "" # Keep track of condition

print("I know these python classes are the highlight of your week!")
while (acceptance != "It's true"): 
    acceptance = input("Confess! ")
```

We can use and/or to make the while statement's **loop condition** more sophisticated. For example, we could allow users to type their passwords in up to three times.

```python
password = "hello"
user_pass = input("Please enter your password")
remaining_attempts = 3

while (user_pass != password) and (remaining_attempts > 0):
    user_pass = input("Try again: ")
    remaining_attempts = remaining_attempts - 1
```

Notice, however, that in this example we don't know why we left the while loop, as it can occur as a result of the user entering the correct password, or having tried too many times. This means we'd need to add additional if statements below. (this can be circumvented by using [break statements](/material/week-4/extensions#break-and-continue)

We often use while statements to do things a certain number of times (again, using a counter):

```python
# Remove all 4 limbs 
limbs_remaining = 4

while limbs_remaining > 0: # Run until the blacknight dies
    print("Come on then!") 
    limbs_remaining = limbs_remaining - 1 # Increment the counter

print("We'll call it a draw")
```

Finally, we can use such counters to loop through lists using a while statement:

```python
my_list = ['She', 'sells', 'sea', 'shells']
index = 0 # "counter" to keep track of index

# Loop through our list
while index < len(my_list):
    print(my_list[index])
    index = index + 1
```

But there is a far more natural way of looping through lists in python:

### For

A far more elegant way of looping through data structures that contain some number of elements is to use "for" loops. Think of these as "**for** ***each item*** **in** ***data structure*****"**

```python
# We can use for loops to get things directly from a list
for item in ["Python", "classes", "ftw"]:
    print(item)
```

The same syntax can also be used on dictionaries (and other "iterable" objects) - going through the **keys** by default

```python
my_dict = {"hi" : 1, "fish" : "frog"}

# Can also use for loops to loop through dictionaries
for thing in my_dict: # gives same as my_dict.keys()
    print(thing)
```

### Range

The python **range()** function gives us a useful way of creating a list of numbers. **Note:** the function returns integers ranging from the first parameter to the second, but not including the second!

```python
for num in range(2, 6):
    print(num)
# prints 2 3 4 5
```

Technically range doesn't actually create a list data structure, as such, if you want to create a list of number by using range, you'll need to **typecast** using list()

```python
numbers = list(range(2,14)) # Create a list of integers
print(numbers)
```

Here's an example of using a for loop with range, to print even numbers in a range:

```python
# Print even numbers <= 20
for num in range(0, 20):
    if num % 2 == 0: 
        print(str(num) + " is an even number")
```

And here's a better way (there often is one!):

```python
# Better way to print even numbers <= 20
for num in range(0, 10):
    print(str(num * 2))
```

### Length

If we want to find out how many items are in a data structure we can use the length function, **len()**:

```python
my_list = ['Size', 'doesn\'t', 'matter']

size = len(my_list) # size = 3
```

### Duplicate Search

Here's an example making use of **nested for loops** to find duplicated items in a list -> Here we explicitly use range(len()) so that we can index the list, rather than access its items. We do this because we want to avoid checking the same list index against itself (as this will always be equal):

```python
# Another example - a duplicate search
sentence = ['She', 'sells', 'sea', 'shells', 'on', 'the', 'sea', 'shore']

# Use indices explicitly, to avoid checking the same word against itself
for i in range(len(sentence)):
    for j in range(len(sentence)):
        if (i != j) and (sentence[i] == sentence[j]):
              print(sentence[i] + " is duplicated!")
```


# Extensions

Break, Continue, Enumerate, Strings as Lists, List comprehension

## Break and Continue

Sometimes we want to **exit a for/while loop** before the defining condition is met, or skip some of the iteration steps. **Break and Continue** statements provide this functionality respectively - **Note** it is not good practice to use these unnecessarily, as you can usually just define the **loop condition** differently.

### Break

If we want to exit a loop prematurely, we can use the **break** statement.

```
# break statement use
num = 0
while num < 10:
    if num == 5:
        break
    print(num)
    num = num + 1

# This code will print "0 1 2 3 4" - I.e. the loop  is cut short
```

For an actual usage example, suppose we want to find a prime number greater than 9000, but less than 10000 (not knowing if there is one in this range):

```python
num = 9000
while num < 10000: 
    if is_prime(num): # suppose we have an is_prime() function
        print(str(num) + " is a prime number")
        break
    num = num + 1
```

### Continue

**Continue** allows us to skip iteration steps inside a loop. This might be helpful if we have a complicated for loop which shouldn't carry out its usual operation in a certain case.

For example, we could print the even numbers < 10:

```python
for num in range(0,10):
    if num % 2 != 0: # Number is odd
        continue 
    print(num)
```

**Note:** This is not actually a sensible use of continue, as there are far better ways of printing even numbers in a range (for example, x2 every element in range/2).

## Enumerate

Sometimes we want to iterate through a list, accessing its elements but also keeping track of the corresponding index. Whilst we could use a counter variable to do this manually, Python's built-in **enumerate()** function provides and elegant way of doing this:

```python
my_list["Reduce", "Reuse", "Recycle"]

for index, item in enumerate(my_list):
    print("Item " + index " is " + item)
```

## Strings as lists of characters

Some of you may have noticed that we can use the "in" function on strings as well as lists:

```python
# Remember this?
if "a" in "alex":
    print("Yeehaa")
```

Equivalent to

```python
# What about
if "a" in ['a','l','e','x']:
    print("Strange")
```

For our purposes, we can think of strings as lists of characters, and manipulate them as we would lists - slicing and all!

Here's a simple example demonstrating reverse iteration through a string:

```python
# Strings are a lists
the_dream = "desserts"
university = the_dream[::-1] # Step backwards from end

print(university)
```

And another example showing some contrived list slicing:

```python
# Can use list slicing syntax on strings
my_string = "Luke is on a roll"

daily_mail_string = my_string[0:8] + my_string[11:13] + "t" + my_string[13:]
print(daily_mail_string) # "Luke is a troll"
```


# Week 5

Functions, Import

## Functions <a href="#functions" id="functions"></a>

If we want to do the same thing in multiple places in our code then we can use *functions* rather than *copy pasting.*

This also allows us to **abstract away complexity**, as we can make a function perform some complicated operations, and then forget about how it actually works.

### Definition <a href="#declaration" id="declaration"></a>

We create functions using the **def** keyword - This process is called "definition"

```python
# "Declare" a function
def my_function():
    print("I'm nOT DysFUnctIonaL!")

# "Call" the function twice
my_function()
my_function()

# This will print the sentence twice
```

### Returning <a href="#returning" id="returning"></a>

Most of the time, we use functions to carry out an operation and collect the result. **return** allows us to get the function to give us an output.

```python
# Return lower-case user input from the function
def get_lowercase_input():
    user_input = input("All good in the hood? ")
    user_input = user_input.lower() # Convert to lowercase
    return user_input # <---
```

Then we can use the function, storing the output in a variable

```python
lowered_input = get_lowercase_input()
print(lowered_input)
```

### "Passing arguments" <a href="#passing-parameters" id="passing-parameters"></a>

If we want our function to manipulate some data, we can add **"parameters"** into the definition of the function. Then any time we try and call it, we will need to provide something - doing so is referred to as **passing arguments**.

We've already seen examples of this:

```python
# Some in-built python functions
input("We can pass a string to the input function")
len(['need', 'to', 'pass', 'something', 'to', 'get', 'its', 'length'])
```

In order to add this functionality to our functions, we include the parameters in the definition of the function - specifically, inside the brackets next to the function's name:

```python
# Function prints the first and last character in string
def first_and_last(some_string):
    print(some_string[0] + some_string[-1])

first_and_last("abcde")
# Prints ae
```

{% hint style="info" %}
If the function takes arguments, then we **have to** provide them. (unless we use default arguments - see [extensions](/material/week-5/extensions#default-arguments))
{% endhint %}

It's also important to remember that the type of variable you pass to the function matches what the person who defined it (probably you) expected:

```python
# But, we HAVE to pass something compatible!
first_and_last()     # ERROR - Need to pass something!
first_and_last(1321) # ERROR - We can't use variable[index] on integers
```

It can be a good idea to add [type checking](/material/week-3/extensions#basic-error-handling) to your functions to make sure that the arguments a user passes in are compatible with the operations that your function performs:

```python
# Need to think about these things!
def first_and_last_v2(mebbe_string):
    defo_string = str(mebbe_string) # Throws error if not a valid type cast
    print(defo_string)
```

## Modules and Packages <a href="#modules-and-packages" id="modules-and-packages"></a>

We don't want to reinvent the wheel - If someone has implemented something for us, then we can just use that. But first, some definitions:

* **Module** - a file containing functions. The filename is module\_name.py
* **Package** - a collection of modules
* **Library** - a collection of packages (doesn’t necessarily provide just one functionality)

### Import <a href="#import" id="import"></a>

If we want to use modules/packages in our scripts, then once we've installed them (either using anaconda's navigator software, or[ PIP](/material/week-5/extensions#pip)) we need to **import** them. This is done by using the **import** statement, e.g.:

```python
# "time" is a package which comes with python

import time   # We import the time module
time.sleep(3) # sleep function is part of time module
```

### From <a href="#from" id="from"></a>

Sometimes we don't want to import everything from a package - we can pick specific modules using **from**

* E.g. if we have our own function, also called sleep(), we probably don't want to import the sleep() function from time

Additionally, if we use from, we don't need to explicitly call the function via the module

```python
# All I want to do is sleep
from time import sleep
sleep(3)  # Don't need to call via time module
```

### As <a href="#as" id="as"></a>

If we want to refer to the imported functions or modules using a different name, then we can import them **as** something else

```python
# Zen
from time import sleep as meditate
meditate(3)
```

This can be useful if you have a function with the same name, or if the name of the module/function is annoyingly long

```python
import time as t # Can change module name
t.sleep(3)
```


# Extensions

Default arguments, Recursion, PIP, Terminal

## Default Arguments

If we want a function to use a default value for a parameter, we can use = when defining the functions parameters list:

```python
def greet_user(greeting="Hi", name):
    print(greeting +" " name + ", nice to meet you!")
```

## Recursion

Recursion is a fun and occasionally useful concept. It refers to the ability to have functions **call themselves**

```python
# A recursive function calls itself

# This function will count down to zero
def count_down(number):
    print(number)
    if (number > 0): # Recurse until we reach zero
        count_down(number - 1) # "Recursive" call!

count_down(3) # Will print 3 2 1 0
```

## PIP

You'll often find that the package you want to use is not installed alongside python by default. But if it's popular, then chances are you'll be able to add it using python's package manager, **pip** (pip = "pip installs packages").

pip can be used via the command line / terminal

Installing the numpy package on Windows:![pip](http://localhost:8888/notebooks/week7/pip_windows.png)

and on Linux/Mac:![pip](http://localhost:8888/notebooks/week7/pip_install.png)

## Using the terminal

Much professional programming is done on Linux or mac, both of which are based on UNIX. Windows users can install the UNIX subsystem with fairly little effort (I recommend looking at [https://www.windowscentral.com/how-install-bash-shell-command-line-windows-10)](https://www.windowscentral.com/how-install-bash-shell-command-line-windows-10%29).

"pip install" is an example of a terminal command, but there are many more. Amongst these, the most commonly used are those which allow you to move around inside your PCs directories:

* **ls** lists the files and folders in your current directory, and **ls -a** includes additional information
  * Here -a is an example of an additional parameter. Many commands have these, and you can typically find out about the possible parameters/usage of a command by using the --help tag. For example, "ls --help"
* **cd** short for change directory allows you to move between folders
  * e.g. "cd my\_folder\* moves you into the folder my\_folder, if it exists in the current directory
* **pwd** prints the working directory, so you know where you are in the filesystem
* **mkdir** creates a directory. E.g. "mkdir fish" creates a folder named "fish" in your current directory
* **less** allows you to view the contents of a file without opening it. You can exit this using the "q" key.
  * e.g. if there's a "test.txt" file, you can use "less.txt" to directly view its contents in the terminal window
* **rm** removes stuff - **be careful with this**. The **-r** parameter removes recursively, allowing you to delete folders and their contents
  * e.g. "rm -r my\_folder" deletes the folder "my\_folder"
  * "sudo rm -rf /\*" deletes everything on your pc! :D (the -f parameter forces deletion of protected files)


# Week 6

File I/O and Numpy Array

## File I/O

File I/O (Input/Output) refers to our ability to create / manipulate files via our Python scripts. Though many packages have specific functions for this purpose, Python itself has a convenient set of functions which we can use to manipulate files.

### Open and Close <a href="#open-and-close" id="open-and-close"></a>

Before we can manipulate files, we need to open or create them. To open a file we use the **open()** function and store a "link" to the file in a variable:

* If we want to write to the file: **open("file\_name", "w")**
* If we want to read from the file: **open("file\_name", "r")**
* If we want to append to a file: **open("file\_name", "a")**
* There are more options - see the documentation at <https://docs.python.org/3/library/functions.html#open>

```python
# Create a file for writing, called "my_file.txt"
new_file = open("my_file.txt", "w")
```

{% hint style="info" %}
If a file doesn't exist when we try to open it, then Python will create it
{% endhint %}

Once we're done with using a file, we call **.close()** on the file link to close it

```python
# Close the file
new_file.close()
```

Alternatively, we can use a **with statement** to open a file, do some immediate manipulation, and then have python automatically close it (i.e. the file link can only be used within the indented statement):

```python
with open("my_file.txt", "r") as f:
    # Do something to the file here
    print("hi")
```

### Write <a href="#write" id="write"></a>

The simplest way to add to our file is by using the **.write()** function *on* the variable containing the file link:

```python
new_file = open("my_second_file.txt", "w")

# We can write as many times as we want
new_file.write("Stories outlive their authors")
new_file.write("\n") # Start a new line
new_file.write("So too will these python class materials")

new_file.close()
```

The above example will create the following text file in our **working directory** (typically the same folder as your python script)**:**

{% code title="my\_second\_file.txt" %}

```
Stories outlive their authors
So too will these python class materials
```

{% endcode %}

### Read <a href="#read" id="read"></a>

Sometimes we'd rather read the profound works of others than create our own. To this end, we can use

* **.read(n)** to read the first **n** characters
* **.readline()** to read the first line
* **.readlines()** to read the entire file - stores it as a list, with each line (string) as an element

{% hint style="info" %}
These **read** functions step through the parts of the file that have already been read - so calling .readline() twice in succession will read the first two lines of a file
{% endhint %}

Let's put this to use on some of Gandalf's wise advice:

{% code title="wisdom.txt" %}

```
'I wish it need not have happened in my time,' said Frodo.
'So do I', said Gandalf, 'and so do all who live to see such times.'
'But that is not for them to decide.'
'All we have to decide is what to do with the time that is given us.’
```

{% endcode %}

Supposing the "wisdom.txt" file is in our working directory, we can try reading it:

```python
wisdom = open("wisdom.txt", "r") # Open file

first_six = wisdom.read(6) # Get first six characters
first_line = wisdom.readline() # Get REMAINDER of line
whole_file = wisdom.readlines() # Get REMAINDER of file

wisdom.close()
```

If you'd like to play around with the read/write functionality, here's the file used in the above example:

{% file src="/files/-LoobaiosOyzIYe7zBAV" %}
Gandalf's wise words to Frodo
{% endfile %}

### Reading with For loops

Most often, we'll just use a for loop to read our file, **line by line**. Essentially, the for loop is performing:

```python
for line in file_link.readlines():
    ...
```

but we don't need to specify the conversion of the file to a list of lines, as this is the default behavior:

```python
wisdom = open("wisdom.txt", "r") # Open file

for line in wisdom:
    # Essentially loops through list wisdom.readlines()
    print(line)

wisdom.close()
```


# Week 7

Coding in the "Real World" and Datascience Pt. I - Numpy Basics

The datascience part of this course shall cover the bare minimum of material which should allow you to perform fairly powerful data analysis tasks. A considerably more thorough, and example rich course, can be found at:

{% embed url="<https://jakevdp.github.io/PythonDataScienceHandbook/>" %}

## Coding in the "Real World"

Most new code is written to solve new problems. It is rare that these problems don't build on previous work.

In reality, much of your time will probably be spent figuring out how to repurpose code written by strangers, or colleagues, in order to solve the task in front of you.

If you're lucky, there will be thorough **documentation** for the package / modules in question (and *good comments!*) - This will certainly be the case for all the packages we'll use, as you'll find out ;)

## Data Manipulation - Numpy

Numpy is a powerful package which is used in virtually any scenario where one wishes to perform complicated mathematical tasks on datasets.

### Arrays

Numpy introduces an array datatype which can be used to store data of any dimension in a way more powerful than python's built-in lists.

Arrays allow us to not just neatly store data, but also to handily perform all kinds of manipulations on it, ranging from simple matrix addition, statistics and reshaping, through to Fourier transformations, curve fitting and much more.

We begin by importing numpy with in the conventional fashion, by renaming it to "np", and convert a python list to a numpy array using the **np.array()** function:

```python
import numpy as np

a_list = [[1,2,3],[4,5,6],[7,8,9]]
an_array = np.array(a_list)
```

If we print the list and the array, we see that numpy has recognized the list as a two-dimensional matrix, with rows and columns.

```python
list: 
[[1, 2, 3], [4, 5, 6], [7, 8, 9]]

array:
[[1 2 3]
 [4 5 6]
 [7 8 9]]
```

### Array Slicing

The **shape** of arrays is crucial as numpy allows us to peform a generalized form of [list slicing](/material/week-3/extensions#List-Slicing) on each dimension (or axis) of an array.

For example, the array in the previous section was of shape **(3, 2);** 3 rows and 2 columns.

We can slice (rows, columns) separately, with the same square bracket syntax we'd use for lists

```python
# Slice using array[row_slice, col_slice]

# Print second column
print(a_array[:, 1]) #[every row, column 2]
```

We can also specify a tuple for a specific dimension, which will extract a subset of the elements - I.e. (1, 2) would extract the first and second row/column depending on where in the square brackets we place it:

```python
# We can print the second and third column of the third row:
print(a_array[2, (1,2)])
```

Crucially, this functionality generalizes to higher dimensional arrays, for example, consider the following "cube" in 3D:

![The same logic extends to any number of dimensions](https://158957078-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-LoobEkeCSQp9d3FJslb%2F-LoobZAAC99Hg9Lu22Zg%2F-LoobaoXh_LhTQDjNo4G%2Fimage%20\(5\).png?generation=1568545862459665\&alt=media)

We could represent this as an array in numpy, and slice out a plane (i.e. face of the cube):

```python
# array[x_slice, y_slice, z_slice] etc.
my_array = np.array([[['a', 'b'],['c', 'd']],
                     [['e', 'f'],['g', 'h']]])

# Print the x=0, y-z plane square
print(my_array[0, :, :])
```

### Loading Data with Numpy

Given that numpy gives us a natural and powerful way of storing data in python objects, it should come as no surprise that the package is well-equipped to load data from common data formats such as .csv (comma seperated values) and .txt.

Consider the following dataset, stored as a csv:

{% file src="/files/-LoobaoZJkmhjqBPq4Dn" %}
example\_data.csv
{% endfile %}

| name  | age  | favorite\_number |
| ----- | ---- | ---------------- |
| alex  | 21   | 17               |
| john  | 26   | 32               |
| frank | 37.2 | -238             |
| bob   | 920  | 40.23            |

Here we will use the **.genfromtxt()** function to load the csv file into a "structured array" - this is necessary because the datasets contains different datatypes \[both strings and integers].

{% hint style="info" %}
Numpy arrays can only contain one type of data - E.g. all strings or all integers

If one tries to mix types, then Numpy will store these as "Structured arrays" which behave akin to dictionaries which elements = arrays of a specific type
{% endhint %}

```python
# Use genfromtext to load a csv
my_data = np.genfromtxt("data/exampleData.csv", delimiter=",", 
                         names=True, dtype=None)

# Delimiter tells numpy how columns are seperated
# names=True tells numpy that the first row should be used to label the columns
# dtype=None warns numpy that it needs to figure out the kinds of data itself
print(my_data['age']) # prints [ 21.   26.   37.2 920. ]
```

Often it is more convenient to use the **.loadtxt()** function, which behaves very similar, but works more effectively with files containing only one type of data (i.e. it gives you more direct control of the resulting structure of the array)


# Week 8

Datascience Pt. II: Visualization with Matplotlib


# Week 9

Datascience Pt III: Trend analysis and Curve fitting with Numpy and Scipy


# Week 10

Datascience Pt. IV: Clustering and Regression with Scikit-Learn


# Week 1

Scratch and Thinking Algorithmically

**Note:** Besides setting up your anaconda python environment (see [Setting up Python](/additional/setting-up-python)), there is no "compulsory" homework this week. However, attempting some of the following exercises is highly recommended for people who have never had to develop algorithmic solutions to problems before.

## Scratch

Scratch is a pedagogical programming environment which allows you to create surprisingly complex, interactive games by chaining together many "blocks" of code.

### Tutorial

Start by visiting <https://scratch.mit.edu/projects/editor/?tutorial=getStarted> and completing the scratch tutorial. You will need a fairly modern browser for this to work.

### Building a scratch game

Have a go at creating a simple game in scratch, for example:

* Pong
* Brick Breaker
* Simon Says

The aim here is to learn to feel comfortable with the creation of blocks of instructions which perform the function you have in mind - you don't have to complete an entire game.

{% hint style="info" %}
You can google any of these games and find that many people have already made very fancy scratch versions - try to at least see how they did this by clicking the "see inside" button when viewing their scratch project page
{% endhint %}

### See what's possible

Harvard's CS50 course (on which this course is largely based) sets students a scratch challenge during the first week - you can have a look at some of the things people made at <https://scratch.mit.edu/users/cs50/>

<https://scratch.mit.edu/projects/277537259/> is a particular highlight!


# Week 2

Python Basics - Conditional Logic and Input

## Spyder setup

1. Start by creating a new folder somewhere on your computer called "python\_classes" and then create another folder inside called "week2" - This is where you'll be placing the python scripts (.py files) which you write today
2. Open up Spyder and create a new file inside the *week2* folder called "hello\_world.py"
3. Add a print statement which outputs **hello world** to the console, and make sure that it runs by pressing the green arrow in the toolbar and checking for the print output in the console

## Login system

This week we're going to create a simple login system by using the **input()** function, and if statements.

### Storing user information

For the time being, we're going to store user information in separate variables. Let's start by adding a user with the following details:

***Name:*** Alex\
**Password:** 1234

{% hint style="info" %}
In order to check the name and password in two separate steps, you'll have to store this information in two separate variables
{% endhint %}

### Checking the username

Using the **input()** function, ask users to enter their username, then check that the value they entered **exactly matches** the one in the database (e.g. "Alex") - use print statements to let the user know if they entered an incorrect username

### Checking the password

Now use **input()** again when the user has entered the correct username, but this time ask for the password. If the user succeeds in typing in a correct username-password pair, you should print a *secret* message.

## Extension 1 - Case sensitivity

Using the **.lower()** function (see [extension material](/material/week-2/week-2-extension#sub-component-checking-with-in)), make the username check non-caps sensitive.

{% hint style="info" %}

1. Try and use the function only once, by reassigning the input immediately after gathering it
2. Make sure that your database only contains lowercase usernames - otherwise some people will never be able to login
   {% endhint %}

## Extension 2 - Two users

Using the logical **and/or** operations, add the ability for another user (with a distinct username-password pair) to login

{% hint style="info" %}
You'll need to make use of nested if-statements to ensure the password matches the username
{% endhint %}

We'll see a far more elegant way of adding more users to our database in subsequent weeks (using dictionaries!).

## Extension 3 - Bad Passwords

### Warning

Using the **in** operation (see[ ](/material/week-2/week-2-extension#sub-component-checking-with-in)[extension material](/material/week-2/week-2-extension#sub-component-checking-with-in)), have a go at users who have passwords containing the substrings **"1234"** *or **"*****pass"** *or* **"word",** but only once they've logged in successfully

### Password Change

Instead of just berating the user for their poor choice of password, give them the **option** to **change** their password (i.e. override the old one).

Once they've changed their password, print it out to confirm it worked.


# Week 3

Lists, Dictionaries, Error Handling and List Slicing

## Login System MKII - New and Improved!

Now that we're armed with the ability to mathematically manipulate user input, and store data in elegant structures, let's revisit and improve the login system from last time.

{% hint style="info" %}
If you didn't manage to finish the final extension exercise last time, or have lost your code, then feel free to use our [solution](/solutions/solutions#extension-3-bad-passwords)
{% endhint %}

### Captcha

Let's have a go at utilizing typecasting, by asking the user to verify a simple Captcha before allowing them to try to login.

Something like: "What is 4 = 21?" will suffice for now.

### A better database

Store three username-password, key-value pairs in a dictionary, and use this dictionary to directly simplify the logic of username and password checking (i.e. **don't** store username-password information for in distinct variables, even for *if-statement* purposes).

{% hint style="info" %}
You'll still want to check that the username is correct before checking the password. It'll be helpful to remember that:

1. You can check which keys (usernames) are stored in a dictionary using the .keys() member function
2. You can access values corresponding to a key using square brackets: dictionary\[key]
   {% endhint %}

### More bad passwords

Use a list to store some of what you'd consider "bad passwords" and then check that the usernames password doesn't match any of these - make sure this check ***isn't case sensitive**)*

### Customized Welcome Messages

Use another dictionary to store different welcome messages, corresponding to different users - print these out to users once they've successfully logged in.

## Extension 1 - Incompetent User Warning

Use error handling to ensure that your login system berates the user if they don't enter an actual number when attempting the Captcha (rather than just crashing and throwing a ValueError).

## Extension 2 - List Slicing Fun

Using the join and split function, we're going to find a hidden message in the following poem:

*hidden from all to see, trapped in an eternal night, those shadows plain surrounding me, no sunrise in sight*

To do this, carry out the following:

1. Split the poem into a list of words
2. Use list slicing to create a new list containing items 0, 6, 12 ,18 from the list you created (i.e. step-size 6)
3. Use **join()** to print this list out - hopefully revealing a secret message

## Extension exercise 3 - Forced Palindromes

Take an input word from the user, and "palindromify" it - *\*\**&#x55;tilize the fact that list slicing can also be similarly applied to strings

For example: "frog" -> "frogorf"

## Extension 4 - Random Captcha

### Random Numbers

random()

### Random String

ASCII character - typecast integer to character with (chr) 65-125 [ASCII Table](http://www.asciitable.com/)


# Week 4

While, For and Challenges!

## 1 While Loops - Finishing our Login System!

### 1.1 Have mercy on the user

Using a while loop allow the user to keep entering their password until they get it right (do this without breaking all the functionality you implemented in the past - feel free to use our solutions).

### 1.2 That was too much mercy

Limit the number of attempts the user gets by using a counter, as we discussed in the slides.

### 1.3 More secure than Hillary's inbox

We don't want Russian hackers breaking through! Use the Captcha code from last time, and force the user to enter a Captcha after every failed password attempt.

## 2 For loops

### 2.1 Hip to be Square

Take a number N from the user, and print an NxN square using characters 'X'

#### 2.1 Extension

Print a "hollow" square (i.e. only the edges are "X")

### **2.2 Hidden message with for loop**

Use **.split(" ") \[**&#x73;ee [extensions](/material/week-3/extensions#Split)] and print out all words in the following phrase which start with a capital letter:

*it was The best of times, it was the worst of times, it was the age of wisdom, it was the age of Foolishness, it was the epoch Of Belief, it was the epoch of incredulity, it was the season of light, it was the season of darkness, it Was The spring of Hope, it was the winter Of despair, we had everything before us, we had nothing before us, we were all Going direct To Heaven, we were all going direct the other way—in short, the period was so far like the present period, that some of its noisiest authorities insisted on its being received, for good or for evil, in the superlative degree of comparison only.*

### 2.3 Pythagorean triples - Three times the fun!

Pythagorean triples are sets of three integers which satisfy Pythagoras' theorem:

* a\*\*2 + b\*\*2 = c\*\*2

For example, the first Pythagorean triple is (3, 4, 5).

Find all Pythagorean triples where c < 100 and print these out (ignoring duplicates in a and b), formatted as: \[3, 4, 5] , \[a2, b2, c2] , \[a3, b3, c3] ...

## Extension 1 - Goat Latin

Implement goat Latin (a made up version of english \[this is a facebook interview question!]), such that a user can input a normal english sentence, and your program returns the goatified version:

* E.g.: "word1 word2 word3 word4 word5..." -> "aword1 aaword2 aaaword3 aword4 aaword5..."

Instead of adding unlimited extra 'a's with increasing word index, add one, then two, then three, and then one again, etc.

### **Extension 1 Extra**

1. If a word begins with a vowel, append "ma" to the end of the word.
   * "apple" becomes "applema"
2. If a word begins with a consonant (i.e. not a vowel), remove the first letter and append it to the end, then add "ma".
   * "goat" becomes "oatgma"
3. Add one letter 'a' to the end of each word per its word index in the sentence, starting with 1.
   * Example, the first word gets "a" added to the end, the second word gets "aa" added to the end and so on.

**Note:** You may find the following list useful: vowels = \['a', 'e', 'i', 'o', 'u']

## Challenges

These challenges are tricky and require lots of clever steps! Don't feel disparaged if you don't manage to finish them - they are fairly mathematical, and it takes a lot of practice to develop the patterns of thought which computer scientists use to tackle such glorified logic puzzles.

However, we **strongly** recommend that you ask friends, or even us (contact us via the Facebook HackSoc group) before you look at the worked solutions - sometimes people are closer to the solution than they realise and all they need is a few helpful hints (google "rubber duck debugging" ;) )

### Challenge 1 (Modulu) - Fizz Buzz <a href="#challenge-1-modulu-fizz-buzz" id="challenge-1-modulu-fizz-buzz"></a>

This is a classic problem which you'll probably encounter many times if you learn other programming languages in future.

#### Problem Statement <a href="#problem-statement" id="problem-statement"></a>

Given an integer N (provided by the user), print out all the integers from 0 to N. However, you must replace numbers as follows:

* If a number is a multiple of 3, then print **Fizz** instead of the number
* If a number is a multiple of 5, then print **Buzz** instead of the number
* If a number is a multiple of 3 **and** 5, then print **Fizz Buzz**

#### Extension <a href="#extension" id="extension"></a>

* Instead of using the built-in % operator from python, implement it yourself using another while loop - Can't trust the python developers!
* Make sure that your program can handle **edge cases** - This is extremely important for real programmes

### Challenge 2 (While + List Slicing) - Pascal's Triangle <a href="#challenge-2-while--list-slicing-pascals-triangle" id="challenge-2-while--list-slicing-pascals-triangle"></a>

Here you will create a piece of code which generates Pascal's triangle

#### Base Problem Statement <a href="#base-problem-statement" id="base-problem-statement"></a>

Given an integer N (provided by the user), using a while loop print out Pascal's Triangle in the format: 1, 121, 1331, 14641,... row\_N\
\[Try doing this without creating more than one list per loop step for more difficulty]

#### Extra Challenge <a href="#extra-challenge" id="extra-challenge"></a>

* Use *Nested Lists*! Store each row as a list inside of a larger list, e.g.: \[\[1], \[121], \[1331],... \[row\_N]]
* After you've created your pascal's triangle list, print it out so it looks like an actual triangle:

  ```
    1  
  1 2 1 ...
  ```

(Note: strings can be multiplied, e.g. "Hiiiiiiii" = "H" + "i" \* 8)


# Week 5

## 0 Finish last weeks stuff!

There were a lot of exercises last week, try and finish the for/while loop exercises before attempting this weeks material on functions (don't worry about completing the challenge problems though).

## 1 Fun with Functions

### 1.1 Maximal entertainment <a href="#maximal-entertainment" id="maximal-entertainment"></a>

Create a function which takes in a list and prints out the biggest value in the list (**don't** use pythons max() function!)

**Extension**

Instead, **return** the maximum value and print it outside of the function

### 1.2 Sum thing to do <a href="#sum-thing-to-do" id="sum-thing-to-do"></a>

Create a function which accepts a **list** of numbers and prints the **sum** of all the numbers

**Extension**

Get your function to return the **average** value in the list (You'll want to use len())

**1.3 Developing a range of skills**

Create a function which mimics the function of range(), by returning a list of integers from the minimum value to the maximum value - 1 (i.e. my\_range(min, max))

**Extension 1**

Use **default arguments** such that if the "min" argument is not given, the function starts counting from zero

**Extension 2**

Try and implement the range function using recursion (you can use another function to do the actual list creation, if you like, so long as it uses recursion!)

## 2 Import-ant Syntax

Import the following two packages, renaming them, such that you can get the following code snippet to run:

* Import "numpy", renamed to "np"
* Import the "pyplot" module from the "matplotlib" package, renamed to "plt"

Then run:

```python
# Scary block of awful code
plt.scatter([0.35 * (2/7) + 0.15, 0.5 - 0.35 * (2/7) ], [2, 2], c='b', marker='s', s=300 )
plt.scatter(0.325, 0.75, c='k', marker='^', s=150);l = np.arange(0.15, 0.51, 0.01)
plt.plot(l ,np.sin(np.linspace(np.pi, 2*np.pi, len(l))), c="r")
plt.axis('off');plt.margins(0.25);plt.title("?sracs eseht tog I woh wonk annaW"[::-1])
plt.show()
```

You'll know when it's working ;)


# Week 6

File I/O and Consolidation

## Note

As this is the last week of "Python Basics", there is a large consolidation exercise (number 3) which we highly recommend you try and do prior to next week's session - Solutions will be released by Thursday evening!

## 1 Basic File I/O

#### Create <a href="#create" id="create"></a>

Create a file called musings.txt and write some some of your thoughts into it (be sure to write **multiple lines**)

#### Read <a href="#read" id="read"></a>

Close and reopen the musings.txt file, and use a for loop to go through and print out every line)

## 2 Wherefore art thou paragraph

{% file src="/files/-LoobaVPlI7b2LdnTjFK" %}
Shakespeare Text File
{% endfile %}

Read the shakespeare.txt file:

* Create a function which returns the first **N** paragraphs from the file
* Make use of the fact that the line preceding each paragraph contains the corresponding paragraph number

There are two approaches to this problem:

1. Use .readlines() and treat this as a list manipulation problem
2. Use loops and .readline() to "walk through" the file, and format it as you go

**The second approach is more instructive!**

{% hint style="info" %}

1. Use a while loop to carry out .readline() until you reach the end of a paragraph &#x20;
2. Create a function which returns one paragraph, and put it inside another function which calls it N times
   {% endhint %}

## 3 Caesar's Cipher Consolidates Cool Class

Hold on to your hats, because this is a big one! To complete this exercise you'll need to use almost everything we've learned so far, so make sure you're sitting comfortably (preferably with a caffeinated beverage at hand) and let's go!

{% hint style="warning" %}
This is quite a tricky exercise, but I strongly suggest you give it a go, as we'll be moving onto specialized topics next week, and an understanding of the python "basics" will be key.
{% endhint %}

The goal of this exercise to to create a function which will decipher files, given a decode word, and it's frequency. To succeed you should find the source of each of the encrypted messages (the file names offer a slight clue)

More specifically, you will be provided with a word in normal English, and the number of times it is used in the paragraph, and must use this to find out the shift of the alphabet that has been applied.

{% hint style="info" %}
You'll want to count the frequency of each encrypted word, and find the one that matches the decode word - dictionaries might be a helpful way of doing this ;)
{% endhint %}

The type of cipher being used is the simple Caesar Cipher, which works by shifting the alphabet:

{% embed url="<https://cryptii.com/pipes/caesar-cipher>" %}

The files containing the encrypted messages, and there corresponding decode word, are given below:

For Mary, the decode word is "the" and the frequency is 4

{% file src="/files/-LoobaVTfyEssQxXr70Z" %}
Mary.txt -  Decode "the" x 4
{% endfile %}

For Charles, the decode word is "of" and the frequency is 12

{% file src="/files/-LoobaVVioNJ8hT5lAgC" %}
Charles.txt - Decode "of" x 12
{% endfile %}

For Agatha, the decode word is "train", and the frequency is 2

{% file src="/files/-LoobaVXLxBU4KXpl5dY" %}
Agatha.txt - Decode "train" x 2
{% endfile %}

### Challenge

For Jane, you are given that "is" and "in" occur the same number of times within the text

{% file src="/files/-LoobaVZs44PDHhrww59" %}
Jane.txt - Decode #"is" = #"in"
{% endfile %}

{% hint style="danger" %}
Be careful with punctuation in both of the following approaches!
{% endhint %}

### **Decode Approach 1 - Alphabet List**

Use the alphabet list provided below, and the **.index()** function, to find the shift being applied in the cipher:

{% hint style="info" %}
You'll find the **.index(element)** function useful - when called on a list, it returns the first index at which a matching element was found

Here's a list of the alphabet as well:

alphabet = \['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z']
{% endhint %}

### **Decode Approach 2 - ASCII Conversion**

Using the typecasts which convert characters (strings of length 1) to ASCII numbers, and vice versa, find the shift being applied by the cipher.

{% hint style="info" %}
Typecasts:

* **Character -> (ASCII) Integer:**  ord()
* **(ASCII) Integer:** chr()
  {% endhint %}

\*\*\*\*


# Week 7

Numpy array basics

## 1 Alex's Awry Array Ailment

In this exercise you'll need to use list slicing and some previously unintroduced numpy functions to achieve a specific goal

Your aim is to create a function which creates a numpy array of dimension (NxM) \[These are the parameters of the function] consisting of alternating 1s and 0s

**You are not allowed to do this using lists or for-loops, instead use:**

* **np.ones(n,m) -** creates a n x m matrix of 1s
* **np.zeros(n,m)** - creates a n x m matrix of 0s

## 2 Loading data with numpy

Use **np.genfromtext()** to load the data we've provided for you. For this exercise you'll want to use the "Deaths"data.

In order to get the desired behaviour, you'll need to specify some arguments in **np.genfromtxt()**, as we did in the example (What's the delimiter?, Is there a column names row?, Are there multiple types of data in each row?).

{% hint style="info" %}
See <https://docs.scipy.org/doc/numpy/user/basics.io.genfromtxt.html>
{% endhint %}

1. Once you've imported these two data sets successfully, double check that you can access and print items from the numpy arrays in the same was we did with lists (i.e. using a for loop and \[] index notation)
2. Add a column to the array which contains the fraction of deaths attributed to males


# Week 8

Plotting a dataset

## Hatching a Plot <a href="#hatching-a-plot" id="hatching-a-plot"></a>

{% file src="/files/-LoobaVQKxfAOikH33ir" %}
Alcohol Price Data
{% endfile %}

{% file src="/files/-LoobaVSTKHSzGS8Xdcl" %}
Deaths Data
{% endfile %}

{% file src="/files/-LoobaVUHEd-1Zx92yzm" %}
Drug Prescription Data
{% endfile %}

{% file src="/files/-LoobaVWX9QSCj0eUW\_a" %}
Household Expenditure
{% endfile %}

### Part 1: Loading data with numpy <a href="#part-1-loading-data-with-numpy" id="part-1-loading-data-with-numpy"></a>

Use **np.genfromtext()** to load the data we've provided for you. For this exercise you'll want to use the "Deaths" and "Household\_expenditure" data.

In order to get the desired behaviour, you'll need to specify some arguments in **np.genfromtxt()**, as we did in the example (What's the delimiter?, Is there a column names row?, Are there multiple types of data in each row?).

* Once you've imported these two data sets succesfully, double check that you can access and print items from the numpy arrays in the same was we did with lists (i.e. using a for loop and \[] index notation)

### Part 2: Plotting with Matplotlib <a href="#part-2-plotting-with-matplotlib" id="part-2-plotting-with-matplotlib"></a>

You'll want to start by importing matplotlib.pyplot as plt, because no one wants to write so much stuff everytime they use the module. Then

* Using for loops, create lists containing the years, total spending and percentage spending from the Household expenditure array
* Plot the total spending and percentage spending against years, in two seperate plots
* Make your plots look nice by setting labels and titles using plt.title("my title"), plt.xlabel("x axis label")

**Extension:**

* Plot both the percentange and total spending on the same plot, first normalizing (i.e. rescaling) the data, so only the trend information is seen.
* Using the label="my label" argument in the plotting step, create a legend (you'll need to call plt.legend() before plt.show())

### Part 3: Bar Charts <a href="#part-3-bar-charts" id="part-3-bar-charts"></a>

Using the **plt.barh(bin\_labels, bin\_frequencies)** function, create a barchart visualizing the relative frequencies of alchol-related deaths in 2015 (i.e. the data imported from the Deaths.csv)

* You'll need to use the first column of the data as the axis labels

## A fitting challenge <a href="#a-fitting-challenge" id="a-fitting-challenge"></a>

Choosing an interesting dataset (e.g. Alcohol-related expenditure), try to fit a polynomial function to the data using **np.polyfit** (see: [https://docs.scipy.org/doc/numpy/reference/generated/numpy.polyfit.html)](https://docs.scipy.org/doc/numpy/reference/generated/numpy.polyfit.html%29), and based on the best fitting polynomial, extrapolate the trend for the next five years - plot this prediction and see whether it makes sense!


# Week 2

Solutions to Python Basics - Conditional Logic and Input

## Exercise 1 - Login System

Here we create a simple username-password checker by comparing **user input** to **stored variables,** using **nested** if-statements.

{% code title="week2\_solutions\_ex1.py" %}

```python
# Store the name and password in 2 separate variables
name = "Alex"
password = "1234"

# First get the user to enter their name
input_name = input("Enter your username: ")

# Check against the name we have stored in 'name'
if input_name != name:
    # The input_name does not equal the name
    print("You have entered an incorrect username")
else:
    # The input_name matches the name -> ask for password
    input_password = input("Enter your password")

    if input_password == password:
         # Password is correct -> print a secret message
         print("this is a secret")
     else:
         # Password didn't match -> print an error
         print("You have entered an incorrect password")
```

{% endcode %}

## Extension Exercise 1 - Case Sensitivity

Here we use the **.lower()** function to ensure that our system isn't sensitive to capitalization in usernames - this also means that we need to store lowercase usernames in our "database".

Note that the **\*\*.lower() function** does not reassign\*\* the value of a variable automatically, so we have to reassign manually.

{% code title="week2\_solutions\_ext1.py" %}

```python
# Store the name and password in 2 separate variables
name = "alex" # name must be lowercase
password = "1234"

# First get the user to enter their name
input_name = input("Enter your username: ")
input_name = input_name.lower() # Convert to lowercase

if input_name != name:
    # input_name does not equal the name
    print("You have entered an incorrect username")
else:
    # input_name matches name -> ask for password
    input_password = input("Enter your password")

    if input_password == password:
         # Password is correct -> print a secret message
         print("this is a secret")
     else:
         # Password didn't match -> print an error
         print("You have entered an incorrect password")
```

{% endcode %}

## Extension Exercise 2 - Two users

The aim of this extension and the next is to become comfortable with many different levels of nested statements, and more complicated logical expressions.

**Note:** These examples are fairly ugly and contrived - in future weeks we will see how we could make this system far more efficient, and our code far more elegant.

{% code title="week2\_solutions\_ext2.py" %}

```python
# For each user, store the name and password seperately
# Again, name must be lowercase
name1 = "alex"
password1 = "1234"

name2 = "joe"
password2 = "5678"

# First get user to enter their name
input_name = input("Enter your username: ")
input_name = input_name.lower() # Convert to lowercase

# Check if the username matches one we have stored
if input_name == name1 or input_name == name2:
    # Valid username -> Ask for password
    input_password = input("Enter your password: ")

    # Now check that the password matches for the corresponding user
    if input_name == name1 and input_password == password1:
        print("Welcome, First user")
    elif input_name == name2 and input_password == password2:
        print("Hey there, Second user")
    else:
        # Password didn't match 
        print("You have entered an incorrect password")
else:    
    # Username didn't match
    print("You have entered an invalid username")
```

{% endcode %}

## Extension 3 - Bad Passwords

This is a fairly grueling exercise, but quickly recognizing the logical exclusivity which is implied by the **indentation level** of a piece of code is an essential skill for python programmers - so try and make sure you fully understand why this solution works!

{% code title="week2\_solutions\_ext3.py" %}

```python
# For each user, store the name and password seperately
# Again, name must be lowercase
name1 = "alex"
password1 = "1234"

name2 = "joe"
password2 = "5678"

# First get user to enter their name
input_name = input("Enter your username: ")
input_name = input_name.lower() # Convert to lowercase

# Check if the username matches one we have stored
if input_name == name1 or input_name == name2:
    # Valid username -> Ask for password
    input_password = input("Enter your password: ")

    # Now check that the password matches for the corresponding user
    # Do it in one big check, so we don't have to repeat the password check
    if ((input_name == name1 and input_password == password1) or
        (input_name == name2 and input_password == password2)):
        print("Welcome, " + input_name)

        # Check if the password contains a bad component
        if ("1234" in input_password or 
            "pass" in input_password or 
            "word" in input_password):
            # Ask if they want to change their password
            change_password = input("Warning: Password Sucks, change?(y/n): ")

            if change_password == "y":
                # Get a new password, and print it out to show its changed
                new_password = input("Enter new password: ")

                # Need to change the correct password
                if input_name == name1:
                    password1 = new_password
                else: # Know this must be user2
                    password2 = new_password

                print("Succesfully change password to :"  + new_password)                   

    else:
        # Password didn't match 
        print("You have entered an incorrect password")
else:    
    # Username didn't match
    print("You have entered an invalid username")
```

{% endcode %}


# Week 3

Solutions to Lists, Dictionaries, Error Handling and List Slicing

## Exercise 1 - Login System MKII

Here we create a new and improved login system, by making use of the different **data structures** we have learnt about, **dictionaries** and **lists.**

{% code title="week3\_solutions\_ex1.py" %}

```python
# Store the username and password as key-value pairs 'username : password'
users = {"alex" : "1234", "joe" : "5678", "luke" : "0000"}

# store the bad passwords in a list
bad_passwords = ["password", "pass", "word", "1234"]

# store custom welcome messages for each user in users
welcome_msgs = {"alex" : "Greetings Alex", 
                "joe" : "Howdy Joe", 
                "luke" : "Good day Luke"}

# Add a captcha by getting the user to answer a question
answer = input("What is 4 + 21? ")    # always reads a string
answer = int(answer)    # convert string input to integer

# if captcha answer was wrong, print error message and quit program
if answer != 25:
    print("You are not human!")
    exit()    # stops the script from being run

# Now get user to enter their name
input_name = input("Enter your username: ")
input_name = input_name.lower() # Convert to lowercase

# check if the username exists in our users-password dictionary
if input_name in users.keys():
    # input_name exists so check password matches
    input_password = input("Enter your password:")
    if input_password == users[input_name]:
        # password is correct, print custom welcome message
        print(welcome_msgs[input_name])

        # check if the password is in the bad list
        # use lower() to ignore case when checking for bad passwords
        if input_password.lower() in bad_passwords:
            # password is bad so give the option to change it
            change_password = input("Warning: Password sucks, change?(y/n):")
            if change_password == "y":
                # user wants to change, so get a new one
                new_password = input("Enter new password:")
                users[input_name] = new_password  # store for that user
                print("Password changed to:" + new_password)
    else:
        # password invalid
        print("You have entered an incorrect password")
else:
    # input_name is not in the users dictionary
    print("Unknown username")
```

{% endcode %}

## Extensions

The code for the extensions can be inserted into the solution above between the preceding and subsequent lines of code -- see the comments in the code for which lines these are.

## Extension 1 - Incompetent User Warning

We put the integer conversion into a try except block so that we can stop the program from reporting an error when a non-integer answer is entered.

{% code title="week3\_solutions\_ext1.py" %}

```python
.............
answer = input("What is 4 + 21? ")    # preceding line
# input always returns a string, so we need to convert it to an integer

# attempt to convert the inputted string to an integer
try:
    answer = int(answer)
except ValueError:
    print("You are incompetent! That was not an integer!")
    exit()

if(answer != 25):    # subsequent line
.............
```

{% endcode %}

## Extension 2 - List Slicing Fun

We take the poem as a string and split it up using the **split(' ')** method, splitting on spaces. Then we get every sixth word in the list using **list slicing**, rejoin the list together and print the result.

{% code title="week3\_solutions\_ext2.py" %}

```python
poem = "hidden from all to see, trapped in an eternal night, those shadows plain surrounding me, no sunrise in sight"

# split the poem into a list of words
poem_list = poem.split(' ')

# split the list on every 6th word
new_poem_list = poem_list[::6]

# create a new poem from the list of words
new_poem = " "  # space is our separator
new_poem = new_poem.join(new_poem_list)
print(new_poem)
```

{% endcode %}

## Extension 3 - Forced Palindromes

We ask the user to input a word, and print out a palindrome version of the word, using list slicing by reversing the word and not including the first reversed character

{% code title="week3\_solutions\_ext3.py" %}

```python
# Get a word from the user
user_input = input("Enter a word:")

# reverse the word and don't include the first reverse character
new_word = user_input + user_input[::-1][1:]
print(new_word)
```

{% endcode %}

## Extension 4 - Random Captcha


# Week 4

Solutions to While, For and Challenges

## Exercise 1 - Finishing Login System

Here we finally finish the login system by making use of while loops to let the user have multiple attempts at entering the password correctly.

{% code title="week3\_solutions\_ex1.py" %}

```python
# Store the username and password as key-value pairs 'username : password'
users = {"alex" : "1234", "joe" : "5678", "luke" : "0000"}

# store the bad passwords in a list
bad_passwords = ["password", "pass", "word", "1234"]

# store custom welcome messages for each user in users
welcome_msgs = {"alex" : "Greetings Alex", 
                "joe" : "Howdy Joe", 
                "luke" : "Good day Luke"}

# Add a captcha by getting the user to answer a question
answer = input("What is 4 + 21? ")    # always reads a string
answer = int(answer)    # convert string input to integer

# if captcha answer was wrong, print error message and quit program
if answer != 25:
    print("You are not human!")
    exit()    # stops the script from being run

# Now get user to enter their name
input_name = input("Enter your username: ")
input_name = input_name.lower() # Convert to lowercase

# check if the username exists in our users-password dictionary
if input_name in users.keys():
    # input_name exists so check password matches
    input_password = input("Enter your password:")
    remaining_attempts = 3

    # if the password is wrong and we have attempts left, ask again
    while (input_password != users[input_name]) and (remaining_attempts > 0):
        # re-enter the captcha
        captcha_answer = input("Wrong password! What is 7 + 36? ")
        captcha_answer = int(captcha_answer)    # convert to integer
        # captcha was wrong, so stop them trying
        if captcha_answer != 43:
            print("You are not human!")
            break

        input_password = input("Try again:")
        remaining_attempts = remaining_attempts - 1

    if input_password == users[input_name]:
        # password is correct, print custom welcome message
        print(welcome_msgs[input_name])

        # check if the password is in the bad list
        # use lower() to ignore case when checking for bad passwords
        if input_password.lower() in bad_passwords:
            # password is bad so give the option to change it
            change_password = input("Warning: Password sucks, change?(y/n):")
            if change_password == "y":
                # user wants to change, so get a new one
                new_password = input("Enter new password:")
                users[input_name] = new_password  # store for that user
                print("Password changed to:" + new_password)
    else:
        # password invalid
        print("You have run out of password attempts!")
else:
    # input_name is not in the users dictionary
    print("Unknown username")
```

{% endcode %}

## Exercise 2.1 - Hip to be Square

Here we print a square of 'X' with a side length entered by the user

{% code title="week3\_solutions\_ex2\_1.py" %}

```python
# request a number, entered by the user
size = input("Enter a number: ")
size = int(size)  # Convert to int, assuming the input was an integer

# print size amount of 'X', size times - to produce size x size square
for number in range(size):
    print("X " * size)

# print hollow square
# print top border - a line of X, then print middle - 'X spaces X',
# then bottom border
print("X " * size)
for number in range(size-2):
    print("X " + "  " * (size-2) + "X ")
print("X " * size)
```

{% endcode %}

## Exercise 2.1 Extension - Hollow Square

This time we print a hollow square

{% code title="week3\_solutions\_ext2\_1.py" %}

```python
# request a number, entered by the user
size = input("Enter a number: ")
size = int(size)  # Convert to int, assuming the input was an integer

# print top border - a line of X, then print middle - 'X spaces X',
# then bottom border - a line of X
print("X " * size)
for number in range(size-2)
    print("X " + "  " * (size-2) + "X ")
print("X " * size)
```

{% endcode %}

## Exercise 2.2 - Hidden message via For Loop

Split the message into words and put into a list. Then we loop through them all and only add them to the secret message if the word begins with an uppercase character.

{% code title="week3\_solutions\_ex2\_2.py" %}

```python
# the secret message
message = "it was The best of times, it was the worst of times, it was the age of wisdom, it was the age of Foolishness, it was the epoch Of Belief, it was the epoch of incredulity, it was the season of light, it was the season of darkness, it Was The spring of Hope, it was the winter Of despair, we had everything before us, we had nothing before us, we were all Going direct To Heaven, we were all going direct the other way—in short, the period was so far like the present period, that some of its noisiest authorities insisted on its being received, for good or for evil, in the superlative degree of comparison only."

words = message.split(" ")
secret_message = ""
for word in words:
    if word[0].lower() != word[0]:
        secret_message = secret_message + word + " "

print(secret_message)
```

{% endcode %}

## Exercise 2.3 - Pythagorean Triples

Here we print all the Pythagorean Triples which sum to less than 100 using for loops to calculate them. To check that c\*\*2 = a\*\*2 + b\*\*2, we get the square root of a\*\*2 + b\*\*2 which will be a decimal value, and we check that it equals the integer version, e.g. 5.0 == 5.

{% code title="week3\_solutions\_ex2\_3.py" %}

```python
# the stored triples
pythag_triples = []

for a in range(1, 101):
    for b in range(a, 101):
        c = a * a + b * b

        # need to check that c is a square number
        # so we check the square root is equal to the int version of sqrt
        if c**0.5 < 100 and c**0.5 == int(c**0.5):
            pythag_triples.append([a, b, int(c**0.5)])

print(pythag_triples)
```

{% endcode %}

## Extension 1 - Goat Latin

We implement Goat Latin (adding extra "a"s to the beginning of words in repeating pattern of 1, 2, 3) by using for loops

{% code title="week3\_solutions\_ext1.py" %}

```python
# Get the user input first
user_input = input("Enter a sentence: ")

result = ""
num_of_a = 1
words = user_input.split(" ")
for word in words:
    # add num_of_a amount of a's to the beginnning of the word
    result = result + "a" * num_of_a + word + " "

    # increment number of a's, but keep value between 1 and 3
    num_of_a = num_of_a + 1    
    if num_of_a > 3:
        num_of_a = 1

print(result)
```

{% endcode %}

## Extension Extra 1 - Advanced Goat Latin

This time, if a word begins with a vowel, append 'ma' to it, otherwise take the first letter, remove it from the beginning and add to the end and then append 'ma' to it. Also add an 'a' to the end of each word per index in the list, e.g. 4 a's for the fourth word.

{% code title="week3\_solutions\_ext2.py" %}

```python
# the vowels
vowels = ['a', 'e', 'i', 'o', 'u']

# Get the user input first
user_input = input("Enter a sentence: ")

result = ""
index = 1
words = user_input.split(" ")
for word in words:
    # word begins with a vowel
    if word[0] in vowels:
        result = result + word
    else:
        result = result + word[1:] + word[0]

    # now add "ma" followed by index number of 'a'
    result = result + "ma" + "a" * index + " "
    index = index + 1    # increment counter

print(result)
```

{% endcode %}


# Week 5

Solutions to Functions and Imports

## Exercise 1.1 - Maximal Entertainment

We create a function to print the maximum value in a given list.

{% code title="week5\_solutions\_ex1\_1.py" %}

```python
# define function to print out the max value in the list 
def print_max_in_list(a_list):
    # initialise the current_best to be the first item
    current_best = a_list[0]

    for item in a_list:
        # update the current best if we see a bigger value
        if item > current_best:
            current_best = item

    print("Max Value:" + str(current_best))

# test the function with the following
# expect '376' to be printed
example_list = [4, 9, 376, 12, 234, 124, 94, 3]
print_max_in_list(example_list)
```

{% endcode %}

## Extension 1.1 - Returning max value

Here we return the max value instead, and then print it.

{% code title="week5\_solutions\_ext1\_1.py" %}

```python
# define function to return the max value in a list
def get_max_value_in_list(a_list):
    # initialise the current_best to be the first item
    max_value = a_list[0]

    for item in a_list:
        # update the current best if we see a bigger value
        if item > current_best:
            max_value = item

    return max_value

# test the function with the following
# expect '376' to be printed
example_list = [4, 9, 376, 12, 234, 124, 94, 3]
print("Max value: " + str(get_max_value_in_list(example_list)))
```

{% endcode %}

## Exercise 1.2 - Sum thing to do

Here we print the sum of all the values in a list

{% code title="week5\_solutions\_ex1\_2.py" %}

```python
# define function to print the sum of all values in a list
def print_sum_of_list(a_list):
    current_sum = 0

    for item in a_list:
        # add the item to the current sum, store it
        current_sum = current_sum + item

    print("Sum: " + str(current_sum))

# test the function with the following
# expected value is 31
example_list = [1, 2, 4, 8, 16]
print_sum_of_list(example_list)
```

{% endcode %}

## Extension 1.2 - Returning Average

Now we average the values in the list and return the result

{% code title="week5\_solutions\_ext1\_2.py" %}

```python
# define function to return the average of values in the list
def get_average_of_list(a_list):
    current_sum = 0

    for item in a_list:
        # add the item to the current sum, store it
        current_sum = current_sum + item

    return current_sum / len(list)

# test the function with the following
# expected value is 6.4
example_list = [1, 2, 5, 10, 14]
print("Average:" + str(get_average_of_list(example_list)))
```

{% endcode %}

## Exercise 1.3 - Developing a range of skills

Here we define a function to mimic the range function - which returns a list of integers from the minimum to the max - 1.

{% code title="week5\_solutions\_ex1\_3.py" %}

```python
# define the range function, which takes a min and max argument
# returns a list of values between min and max-1
def my_range(min_val, max_val):
    the_list = []
    current_val = min_val

    while current_val < max_val:
        the_list.append(current_val)
        current_val = current_val + 1

    return the_list

# test the function with the following
# expect a list of [6, 7, 8, 9, 10, 11]
a_list = my_range(6, 12)
print(a_list)
```

{% endcode %}

## Extension 1.3.1 - Default arguments

Add default arguments to our range function, such that if min is not given then the function counts from 0 - We need to alter the function definition and modify our max and min values

{% code title="week5\_solutions\_ext1\_3\_1.py" %}

```python
............
def my_range(min_val, max_val=None):
    # set max to be min as only 1 argument was given
    # also default min to 0
    if max_val is None:
        max_val = min_val
        min_val = 0

    the_list = []
    current_val = min_val
............

# we can test this by elimintating the first argument in the function call
# expect list of [0, 1, 2, 3, 4, 5, 6, 7, 8]
a_list = my_range(9)
print(a_list)
```

{% endcode %}

## Extension 1.3.2 - Recursive Range function

Here we implement the range function using recursion - the function must call itself, and have a terminating condition.

{% code title="week5\_solutions\_ext1\_3\_2.py" %}

```python
# define the recursive range function
def my_recrusive_range(min_val, max_val):
    if min_val == max_val:
        return []

    return [min_val] + my_recursive_range(min_val+1, max_val)

# test the function with range 0, 10
# expect a list [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
print(my_recursive_range(0, 10))
```

{% endcode %}

## Exercise 2 - Import-ant Syntax

Here we rename some imports and run the 'scary block of awful code'

{% code title="week5\_solutions\_ex2.py" %}

```python
import numpy as np
import matplotlib.pyplot as plt

# Scary block of awful code
plt.scatter([0.35 * (2/7) + 0.15, 0.5 - 0.35 * (2/7) ], [2, 2], c='b', marker='s', s=300 )
plt.scatter(0.325, 0.75, c='k', marker='^', s=150);l = np.arange(0.15, 0.51, 0.01)
plt.plot(l ,np.sin(np.linspace(np.pi, 2*np.pi, len(l))), c="r")
plt.axis('off');plt.margins(0.25);plt.title("?sracs eseht tog I woh wonk annaW"[::-1])
plt.show()
```

{% endcode %}


# Week 6

Solutions to File I/O and Consolidatio

## Exercise 1 - Basic File I/O

Here we create a file, and add some random lines to it. Then we read and print out the file contents.

{% code title="week6\_solutions\_ex1.py" %}

```python
# create a file
my_file = open("musings.txt", "w+")

# write some things to the file
my_file.write("These are some of my thoughts\n")
my_file.write("The solutions are so great\n")
my_file.write("Thanks for these interesting exercises alex\n")

# close the file, so that we can then read from it
my_file.close()

# open the file for reading
my_file = open("musings.txt", "r")

# now read each line in the file, and print it out
for line in my_file:
    print(line)

# close access to the file again
my_file.close()
```

{% endcode %}

## Exercise 2 - Wherefore art thou paragraph

Here we define a function to read N paragraphs from a file, and print them.

{% code title="week6\_solutions\_ex2.py" %}

```python
def read_shakespeare(num_of_paragraphs):
    # open the file, read all the lines and store them
    shakespeare_file = open("shakespeare.txt", "r")
    lines = shakespeare_file.readlines()

    current_paragraph = 1
    line_num = 0

    # keep printing, until we are at the paragraph we want to stop at
    while current_paragraph <= num_of_paragraphs:
        current_line = lines[line_num]

        # remove whitespace from line, as paragraph lines have spaces before
        # the number, check if its the next paragraph
        if current_line.strip() == str(current_paragraph + 1):
            # we have reached the next paragraph
            current_paragraph = current_paragraph + 1
            if current_paragraph > num_of_paragraphs:
                break   # don't print the line if we exceed paragraph 

        # print the line, rstrip removes newline character at the end of the line
        print(lines[line_num].rstrip())
        line_num = line_num + 1

read_shakespeare(3)
```

{% endcode %}

## Exercise 3 - Caesar's Cipher Consolidates Cool Class

{% code title="week6\_solutions\_ex3.py" %}

```python
def decipher(file_name, decode_word, frequency):
    cipher_file = open(file_name, "r")
    cipher = cipher_file.readline()
    cipher_offset = 0

    decode_diff = []
    for index in range(len(decode_word) - 1):
        ascii_diff = ord(decode_word[index+1]) - ord(decode_word[index])
        if(ascii_diff < 0):
            ascii_diff = ascii_diff % 26
        decode_diff.append(ascii_diff)

    words = cipher.split(" ")
    word_count = {}

    # print(decode_diff)

    for word in words:
        if len(word) == len(decode_word) and word in word_count:
            word_count[word] = word_count[word] + 1
        elif len(word) == len(decode_word):
            word_count[word] = 1

    # print(word_count)

    for key in word_count:
        if word_count[key] == frequency:
            # now check letter positions
            correct_positions = True
            for index in range(len(key)-1):
                # check ascii position changes between key and decode_word
                ascii_diff = ord(key[index+1]) - ord(key[index])
                if(ascii_diff < 0):
                    ascii_diff = ascii_diff % 26
                correct_positions = correct_positions & (ascii_diff == decode_diff[index])

            # we have found the word
            if correct_positions:
                print("here")
                cipher_offset = ord(decode_word[0]) - ord(key[0])
                if cipher_offset < 0:
                    cipher_offset = cipher_offset % 26
                break

    decryted_words = []
    for word in words:
        new_word = ""
        for char in word:
            new_char = chr(97 + ((ord(char) - 97 + cipher_offset) % 26))
            new_word = new_word + new_char

        decryted_words.append(new_word)

    plaintext = " ".join(decryted_words)
    print(plaintext)

decipher("mary.txt", 'the', 4)
decipher("charles.txt", 'of', 12)
decipher("agatha.txt", 'train', 2)
```

{% endcode %}


# More Resources

Recommended Reference / Further-Learning Resources

## Reference

### **W3 Schools Python Syntax Tutorials**

As a reference source for brief and clear summaries of different pieces of Python syntax, with interactive examples, we recommend using W3 School's Python tutorials.

{% embed url="<https://www.w3schools.com/python/>" %}

### Stackoverflow

Any problems you encounter which don't seem to have a trivial solution have probably been seen by someone else before - if you're lucky, they'll have posted about it on stackoverflow and gotten someone's advice.

{% embed url="<https://stackoverflow.com/>" %}

{% hint style="warning" %}
Even experienced programmers often use stackoverflow to find solutions to bugs, and then simply copy-paste these into their own code. This is a **bad idea** - always try to *understand the solution*. Then if you encounter a similar problem in the future, or have to adjust the existing solution, you won't be at a complete loss.
{% endhint %}

## Further Learning

### **Codecademy**

Great Python 2 Course with plenty of exercises (there are some minor syntax differences between Python 2 and Python 3).

{% embed url="<https://www.codecademy.com/learn/learn-python>" %}

### **Harvard** CS50

Much of this course is based on the materials used in Harvard's acclaimed CS50 course - these are available online and offer a lot of details which we omit.

{% embed url="<https://cs50.harvard.edu/college/>" %}


# Setting up Python

Anaconda Python Setup

## Anaconda Python Distribution

We'll be using the anaconda python distribution which comes with the **Spyder** **IDE** (Integrated development environment) and a number of useful data-science packages which we'll use later on in the course.

To install Anaconda, simply go to [https://www.anaconda.com/distribution/](https://www.anaconda.com/distribution/#download-section) and download the **Python 3.7** version relevant to your operating system.


# Slides

Lecture Slides

## **Week 1**

{% embed url="<https://docs.google.com/presentation/d/1y6gzENZAbe6mryw1OaW9mFNE2KKbpaKo0XKMnkXwISE/edit?usp=sharing>" %}

## Week 2

{% embed url="<https://docs.google.com/presentation/d/1fmvvyHGcYtklgOswdQvrkVT0on8xU86FKGpxKk0N51o/edit?usp=sharing>" %}

## Week 3

{% embed url="<https://docs.google.com/presentation/d/1DVsVcRNr3-uk5uwvWRSI81gVfBxjATrO9nKRYV1gPDc/edit?usp=sharing>" %}

## Week 4

{% embed url="<https://docs.google.com/presentation/d/1_wAm3q-yUrAD9W3ZY08LPH5Rtk6fOwwDLXlSnA2dATc/edit?usp=sharing>" %}

## Week 5

{% embed url="<https://docs.google.com/presentation/d/1Qsl2Y5q37cMzVq0WC50Mo66p3tBqu77kbm9pUi6Ds4E/edit?usp=sharing>" %}

## Week 6

{% embed url="<https://docs.google.com/presentation/d/1cJwq74RM98JvYHLW8HoQ-TznFXXx7cA59lOq7hzGnFQ/edit?usp=sharing>" %}

## Week 7

{% embed url="<https://docs.google.com/presentation/d/1f9rGYWLDUVTfL1KKDgnFVVEvP0T7rPVpVjdyVEprqBY/edit?usp=sharing>" %}


# Recordings

Pre-recorded lectures on youtube; Feat. Alex and Luke

## Week 1

## Week 2

## Week 3


