---
title: "LSE DS202W 2025: Week 01 - Lab Solutions"
subtitle: "Introduction to Python - Understanding the Basics"
author: "The DS202 Team"
date: 20 January 2025
format: html
editor: visual
jupyter: python3
self-contained: true
---

Here are the solutions to Lab 1.

# Importing libraries

```{python}
import numpy as np
import pandas as pd
from lets_plot import *
LetsPlot.setup_html()
```

# Lists

Lists are a core part of the Python languaged and are used to store collections of items.

We use the `[]` to create a list of values that are comma separated.

Typically, a single list will contain items that share data types. We use `=` to assign 4 three-item lists to 4 respective objects, `x`, `y`, `z` and `s`.

```{python}
x = [22, 30, 42]
y = [6.02214, 3.14159, 6.674]
z = [False, True, False]
s = ["Darth Vader", "Luke Skywalker", "Han Solo"]

print(x,y,z,s)
```

However, we can also store different data types such as integers, floats, booleians, and characters in a single list. `w` contains all four data types we've discussed.

```{python}
w = [40, 5.2, False, "Big Foot"]
print(w)
```

To confirm this, we can use `type` on each item in `w`:

```{python}
print(type(w[0]), type(w[1]), type(w[2]), type(w[3]))
```

To pull out certain items in a list, we can refer to their index. Python uses zero indexing so in `w` the 1st element will have index 0, 2nd 1, 3rd 2, and 4th 3. If we want to find "Big Foot", this is how we do it in Python:

```{python}
w[3]
```

To change elements in the list, we can reference the list and the element and set it equal to a new value.

```{python}
print(s)
s[2] = "Princess Leia"
print(s)
```

# Dictionaries

Another central data structure in Python is the dictionary, which expresses key / value pairs. Suppose we wanted to collect data on infant mortality rate (per 100,000) in Bangladesh, Pakistan and India - we can use dictionaries to collect this data.

We use the `{}` build dictionaries. Keys and values are separated by `:` and entries are separated by `,`.

```{python}
infant_mortality_dict = {"Bangladesh":20.755,"Pakistan":51,"India":26.619}
infant_mortality_dict
```

To find out the infant mortality rate of Pakistan, we simply reference the key to display the value like so:

```{python}
infant_mortality_dict["Pakistan"]
```

If we want to create a new entry in our dictionary, we can partly rely on the same syntax we use to reference already existing entries, only this time, we set the key of the new entry equal to its value.

```{python}
infant_mortality_dict["Sri Lanka"] = 5.6
infant_mortality_dict
```

## Arrays

An array is another common data structure in Python. Arrays are composed of a series of values of the same type, e.g. `character`, `integer`, `float`, `boolean` (i.e `True` and `False`), complex or raw.

We then employ `np.array` to turn these lists into arrays.

👉 NOTE: Arrays are great as (a) they serve as more compact (and thus efficient) versions of lists and (b) we can use `Numpy` to transform these arrays or calculate summary statistics.

Here are some examples of arrays:

```{python}
x = np.array(x)
y = np.array(y)
z = np.array(z)
s = np.array(s)

print(x,y,z,s)
```

Try creating an array from `w`:

```{python}
w = np.array(w)
print(w)
```

🗣️ CLASSROOM DISCUSSION: What happened?

*Remember that arrays can only contain values that share the same type. `w` contains integers, floats, booleans, and characters so the array has simply reverted to a character for the simple reason that you can express an integer, float, and boolean as a character but not, for example, a character as a float.*

You can use arrays of integers or numerics in arithmetic operations or in computations. For example:

```{python}
print(np.sort(y))
```

We can create new vectors by combining previously made vectors. Because we cannot add the elements of two lists without using a for loop (more on this later), we can convert `x` and `y` into arrays using `numpy`.

```{python}
v = (x**2)/2 - y*3 + 5
print(v)
```

We can calculate any quantity of interest we like in Python. Let's calculate the mean of `y` using the `mean` function.

```{python}
y_mean = np.mean(y)
print(y_mean.round(2))
```

Note that when calculating the average, we included `.round(2)`. We (sneakily) used a method which rounded our average to 2 decimal places. `print(y_mean)` will give you the correct answer but with an unnecessary amount of decimal places.

## Sequences

Let's create a sequence of numbers from 1 to 10.

```{python}
a = np.arange(1,10,1)
print(a)
```

Where did 10 go? The range function will start the sequence from the first number inclusively and end 1 unit before the last number.

```{python}
a = np.arange(1,11,1)
print(a)
```

Suppose we want to create a sequence of numbers from -3 to 3 by increments of 0.5.

```{python}
b = np.arange(-3,3.5,0.5)
print(b)
```

You can append the elements of two arrays! We create a list of two arrays and employ `np.concatenate` to do so. We can sort our new array using the `sort` method.

```{python}
a_combined = np.concatenate([a,a])
print(a_combined)
a_combined.sort()
print(a_combined)
```

## For loops

Suppose we want to create a list of strings which tells us the year from 2020 to 2024. This can be achieved using the following lines of code:

```{python}
["The year is " + str(2020),
 "The year is " + str(2021),
 "The year is " + str(2022),
 "The year is " + str(2023),
 "The year is " + str(2024)]
```

👉 NOTE: We write `"The year is " + str(2020)` as we cannot concatenate two objects (wrong word???) together that have a different type.

As you quickly see, this is rather tedious since you copy the same code chunk over and over again. Rather than doing this, you could use a for loop to write repetitive parts of code.

Using a `for` loop, the code above transforms into:

```{python}
# Instantiate a new list
year_strings = []

# Create a range of years
years = range(2020,2025)

# Use the for loop to append items to the list
for year in years:
    year_strings.append("The year is " + str(year))

# Show the output
year_strings
```

The best way to understand this loop is as follows: “For each year that is in the sequence 2019:2024, you execute the code chunk `print("The year is " + str(year))`. Once the `for` loop has executed the code chunk for every year in the vector (i.e sequence 2019:2024), the loop stops and goes to the first instruction after the loop block.

## List comprehensions

A more compact way of looping over elements in a list is by using list comprehensions.

```{python}
["The year is " + str(year) for year in years]
```

👉 NOTE: The differences between `for` loops and list comprehensions may seem trivial with this example, but list comprehensions take advantage of **vectorisation** - which allows users to apply a function over a list in parallel. `for` loops, by contrast, apply a given function over each element in a list which, as you might imagine, is going to take more time and processing power.

# Using functions and list comprehensions

## Functions

In this lab, we’ve encountered and used quite a few different pre-made functions, but sometimes you just need to write your own function to tackle your data, i.e your set/succession of (reproducible) instructions.

A function is simply a code block that performs a specific task (which can be more or less complex), e.g. as calculating a sum.

You should think of writing a function whenever you’ve copied and pasted a block of code more than twice.

You give your function a (meaningful) name (name_of_the_function), define your function arguments (arguments ) i.e the parameters it needs to perform the task it supposed to perform, and put some content in the function. You define how the function should deal with the input/arguments to perform the task it needs to perform in function_content.

Here’s, as an example, of how we can turn our `"The year is " + str([year object])` into a function:

```{python}

def year_string(year):
    return "The year is " + str(year)
```

Use the function inside of a list comprehension.

```{python}
[year_string(year) for year in range(2020,2025)]
```

👉 NOTE: The combination of user-defined functions and list comprehensions is extremely powerful as it is compact and computationally less expensive than using `for` loops. As a result, we are going to be using this combination a lot during this course!

👥 WORK IN PAIRS/GROUPS: Create a list of strings that tell us infant mortality rate in a given country

-   Create an array of countries using `countries = inf_mortality_dict.keys()`.
-   Define a function that concatenates the string, for example Bangladesh:
    -   *The infant mortality rate is 20.755 per 100k in Bangladesh.*
-   Employ the function to the list of countries created in the first step.

```{python}
# Create an array of countries
countries = infant_mortality_dict.keys()

# Define a function
def infant_mortality_string(country):
    return "The infant mortality rate is " + str(infant_mortality_dict[country]) + " per 100k in " + country

# Use function in a list comprehension
[infant_mortality_string(country) for country in countries]
```

## Data frames

Another very common data type is a data frame. Below, we have a very simple example of world population over several decades with data from the World Bank. In this case, we will:

-   Enter a list of values.
-   Create variable labels using a Python dictionary.
-   Convert this dictionary to a Pandas dataframe.

```{python}
years = [1960, 1970, 1980, 1990, 2000, 2010, 2020]
pop = [3.03, 3.69, 4.44, 5.29, 6.14, 6.97, 7.82]

pop_dict = {"year": years, "population": pop}

pop_df = pd.DataFrame(pop_dict)
print(pop_df)
```

Pandas data frames have their own methods and attributes that we will be leveraging for data cleaning and feature engineering. They can also be used to create useful graphs. While there are many graphical libraries in Python, we will be utilizing `lets_plot` which provides a Python analogue to R's `ggplot2` package as it provides a syntax that has unparalleled flexibility. Furthermore, the syntax in both programming languages is nearly identical which means you can transfer your graphing skills over to R easily.

```{python}
gg = (
        ggplot(pop_df, aes("year", "population"))
        + geom_point()
        + geom_line(linetype = "dashed")
        + scale_x_continuous(breaks=np.arange(1960,2030,10))
        + theme(panel_grid_major_x=element_blank())
        + labs(x = "Year", y = "Population (In Billions)", 
            title = "The world's population increased from 3 to 8 billion from 1960 to 2020!")
    )

gg
```

Here is the (rough) equivalent plot using `seaborn` and `matplotlib`.

```{python}
# Import the relevant packages using their common aliases
import matplotlib.pyplot as plt
import seaborn as sns

# Create the plot
plt.figure(figsize=(10, 6))
sns.lineplot(data=pop_df, x="year", y="population", linestyle="--", marker="o")

# Customize x-axis breaks
plt.xticks(np.arange(1960, 2030, 10))

# Add grid and remove major grid lines for x-axis
plt.grid(axis="y", linestyle="--", alpha=0.7)
plt.grid(axis="x", visible=False)

# Add labels and title
plt.xlabel("Year")
plt.ylabel("Population (In Billions)")
plt.title("The world's population increased from 3 to 8 billion from 1960 to 2020!")

# Show the plot
plt.show()
```

👉 NOTE: We adapted ChatGPT to translate the above code. Generative AI tools are pretty incredible in and of themselves and can serve as a game changer for coding. However, we recommend that you use them judiciously - we want you to be able to understand all the code you write!