diff --git a/README.md b/README.md index 36ddfb2..5ff5fd7 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,3 @@ -## This is a course website for 10.020 Data Driven World +## This is a website for the book "Introduction to Computational Thinking and Problem Solving." -You can contribute by forking this repository and creating pull requests ๐Ÿ˜Š +You can contribute by forking this repository and creating pull requests. ๐Ÿ˜Š diff --git a/_Learning_Objectives/overview.md b/_Learning_Objectives/overview.md index abf7e1b..8dc3d51 100644 --- a/_Learning_Objectives/overview.md +++ b/_Learning_Objectives/overview.md @@ -1,5 +1,5 @@ --- -title: 10.020 DDW Learning Objectives +title: Introduction to Computational Thinking and Problem Solving Using Python - Learning Objectives permalink: /lo/weekly key: lo-weekly layout: article @@ -10,101 +10,161 @@ aside: show_edit_on_github: false show_date: false --- - -## Week 1: Python ([Concept Map](https://drive.google.com/file/d/11dFasj8ePnDj0TPYWCRrrWckVQk21fjd/view?usp=sharing)) - -- Apply Python's **procedural** programming and **basic** data structures -- **Define** and **call** functions -- Implement: - - Bubble sort and - - Insertion sort algorithms to **sort** a sequence of number -- Use **print** statements to debug code - -## Week 2: Analysing Programs ([Concept Map](https://drive.google.com/file/d/1PV9-Pe3D1AXhs4pao_70KnF3xyNcNX0P/view?usp=sharing)) - -- Write functions to do **binary heap** data structure operation -- Implement **heapsort** using iteration -- Define **Big-O** notation and other asymptotic notations -- Derive **complexity** of a code using its computation model -- Create **plots** from array data for **visualising** computational time -- **Measure** computation time for bubble sort, insertion sort, built-in sort, and heapsort - -## Week 3: Divide and Conquer ([Concept Map](https://drive.google.com/file/d/1TRve3OUUgiqjE8DvUDA4breOvj6pmqK2/view?usp=sharing)) - -- Solve problems using **recursion** -- Identify problems that has recursive solutions -- Explain and implement **merge sort** algorithm -- Derive solution of **recurrence** of merge sort using recursion-tree method -- Measure computation time of merge sort and **compare** it with the other sort algorithms - -## Week 4: Object-Oriented Paradigm ([Concept Map](https://drive.google.com/file/d/1iLusuxa-wncnHcxOrRoX4207u07l18Nh/view?usp=sharing)) - -- Create `class` definitions with initialization and other methods -- Create **stored** property and **computed** property -- Draw UML class diagram of a class with attributes and methods -- Explain `has-a` relationship -- Discuss object **aliasing** and **copying** -- Draw UML class diagram for `has-a` relationship -- Implement abstract data type for **Stack**, **Queue** using Object Oriented paradigm -- **Apply** Stack and Queue for some applications -- Implement Queue using double **Stack** and discuss implementation **impact** on computation time - -## Week 5: Searching Data ([Concept Map](https://drive.google.com/file/d/1B91OlTA0Ss2HLDxf_PJcS9O4GZPDRI9K/view?usp=sharing)) - -- Use **Dictionary** to represent graph -- Apply **basic** dictionary operations -- Define **graph**, **vertices**, **edges** and **weights** -- Differentiate **directed** and **undirected** graphs -- Define **paths** -- Create a `Vertex` class and a Graph class -- Represent graphs using **adjacency-list** representation or **adjacency-matrix** representation -- Explain and implement **breadth** first search -- Explain and implement **depth** first search - -## Week 6: Inheritance and Object-Oriented Design ([Concept Map](https://drive.google.com/file/d/1pkxE0M-V7uz_vteyBZsDotkL4sCkJj6b/view?usp=sharing)) - -- Inherit a class to create a **child** class -- Explain `is-a` relationship -- Draw **UML** diagram for `is-a` relationship -- **Override** operators to extend parent's methods -- Implement **Deque** data structure as a subclass of **Queue** -- Implement **Array** and **Linked List** data structure from the same base class - -## Week 8: Visualizing and Processing Data([Concept Map](https://drive.google.com/file/d/1PUZkAsRJLcGxEfqDXC-QQlKa6TQu2oNO/view?usp=sharing)) - -- Create **scatter** plot and statistical plots like box plot, histogram, and bar plot -- Create a **Panda's DataFrame** and selecting data from DataFrame -- Using library to read `CSV` or `EXCEL` file -- **Split** data randomly into training set and testing set -- **Normalize** data using min-max normalization -- Give example of **linear regression and classification** - -## Week 9: Modelling Continuous Data ([Concept Map](https://drive.google.com/file/d/15EkM4XMdMyYjLkg_yFgeKlsgpl-qYjUU/view?usp=sharing)) - -- Write **objective** function of linear regression -- Implement **Gradient Descent algorithm** for optimisation -- Train **linear regression model** using gradient descent -- Transform data for **higher** order features -- Evaluate linear regression model using `r^2` and mean-squared-error -- Evaluate and choose **learning rate** -- Plot **cost** function over iteration time -- Plot **linear** regression - -## Week 10: Classifying Categorical Data ([Concept Map](https://drive.google.com/file/d/1wSyrCyG3fFRR-CSCKfMadnhngANKVrcU/view?usp=sharing)) - -- Write objective function of **logistic** regression -- Use logistic regression to **calculate** probabilities of binary classification -- Train logistic **regression** model -- Split data into **training**, **validation**, and **testing** set -- Visualize **non-linear** decision boundary -- Classify **multi-class** problems using one-vs-all technique -- Calculate **confusion** **matrix**, **precision**, and **recall** - -## Week 12: Design of State Machines ([Concept Map](https://drive.google.com/file/d/1Vql1S6jK7ysFhMvEhQ7LIG5THXoMFYJZ/view?usp=sharing)) - -- Define a **state machine** -- Implement an **Abstract Base Class** for State Machine using abc module -- Define **output** function and **next** state function -- Draw **state transition diagram** and **time-step table** -- **Implement** output function and next state function inside `get_next_values` overridden method. -- **Apply** breadth first search to perform state-space search +## Lesson 0: Computational Thinking and Problem Solving + +By the end of this lesson, students should be able to: +* State the various components of computational thinking, i.e. decomposition, abstraction, pattern recognition and algorithms +* identify the various skills needed in computing and specifically in programming +* state the PCDIT framework for problem solving +* state the difference between novice and expert programmers in solving problems +* explain the need for identifying patterns in problem solving + +## Lesson 1: Code Execution and Basic Data Types +By the end of this lesson, students should be able to: + +* explain how Python code is executed in sequence +* create and use a variable +* create basic data types such as integer, float and string +* display basic data types using print function +* explain the assignment operator +* draw the environment diagram after assignment +* check data type of a literal or variable +* identify input and output data type of a problem + +## Lesson 2: Function, the First Abstraction +By the end of this lesson, students should be able to: + +* call built-in math functions +* explain the purpose of creating a user-defined function +* define a function with and without arguments +* define a function with and without return values +* define a function with multiple arguments +* create a tuple +* define a function that returns a tuple +* access an element of a tuple +* define a function with optional or keyword arguments +* specify data types in arguments and return value +* explain the difference between local and global variables +* choose whether to use local or global variables +* abstract a problem as a function +* identify input, output and process of a function + use print function to debug a function + +## Lesson 3: Basic Operators and Basic Control Structures +By the end of this lesson, students should be able to: +* use basic operators with basic data types +* predict the evaluated data types from an expression +* evaluate math expression with various precedence +* use compound operators +* state the three basic control structures, i.e. sequential, branch and iteration +* identify basic structures from a given problem +* state the Python keywords to be used for each basic control structures +* draw flow chart for sequential, branch and iterative structure +* Derive concrete cases given a problem statement +* Derive design of algorithm steps from some concrete cases + +## Lesson 4: Boolean Data Type and Branch Structure + +By the end of this lesson, students should be able to: +* create a boolean data type +* convert a variable into a boolean data type +* evaluate relational and logical operators +* specify the precedence of relational and logical operators +* implement branch structure using if-else statement +* implement branch structure using if-elif-else statement +* draw a flow chart for if-else and nested if-else +* explain the difference between if-elif and if-if codes +* use assert to create a test +* identify branch structure in a problem +* decompose a problem into multiple selections +* abstract selection process as a function + +## Lesson 5: String + +By the end of this lesson, students should be able to: +* create string using various methods +* create multi-line string +* use basic operations on string data type +* obtain the length of a string +* obtain a character of a string using the index +* create a new substring from a string using slice operator +* explain that string is immutable +* check if a substring is in a string +* Use formatted string literal to display formatted string with data + +## Lesson 6: Iteration using While Loop and For Loop + +By the end of this lesson, students should be able to: + traverse an iterable using for-loop +* enumerating a collection data to get the element and the index +* use range function to create an iterable +* traverse an iterable using its index +* use print to debug while loop and for-loop code +* identify iteration structure from a given problem +* decompose problem into iterative process of smaller problems +* implement simple iteration using while loop +* speciy and identify basic building blocks of a while loop statement +* traverse a string using while loop and counter +* traverse a string with sentinel value +* use a break statement to terminate a loop + +## Lesson 7: List and Tuple + +By the end of this lesson, students should be able to: +* create a tuple +* explain what it means that tuple is immutable +* access an element in a tuple using index +* get the length of a tuple +* check if an item is an element in a tuple +* traverse a tuple +* create a list using square bracket operator +* access an element in a list using index +* get the length of a list +* check if an item is an element in a list +* concatenate a list +* obtain a sublist from a list using the slice operator +* modify an element in a list +* remove an element in a list +* find the position of an element in a list +* create an alias of a list +* clone a list into a new list +* add an element into a list +* pass a list as function arguments +* explain the effect of aliasing for list data type +* create list comprehension +* traverse a list using while loop and for-loop +* draw environment diagram of a list +* use print to display elements of a list +* identify when list or tuple is appropriate in a problem + +## Lesson 8: Nested List and Nested For Loop + +By the end of this lesson, students should be able to: +* create a nested list +* access elements in a nested list +* traverse a nested list using both while loop and for-loop +* draw environment diagram of a nested list +* explain the effect of aliasing of a nested list +* explain the difference between shallow copy and deep copy +* use print to debug nested loop +* identify nested loop structure in a given problem +* decompose nested loop problem into multiple loops + + +## Lesson 9: Dictionary and Set + +By the end of this lesson, students should be able to: +* create a dictionary as key-value pairs +* access the value using the key +* add key-value pair into a dictionary +* use dictionary to implement branch structure +* remove a key-value pair from a dictionary +* check if a key is in a dictionary +* check if a value is in a dictionary +* traverse a dictionary +* compare dictionary with a list +* create a set +* use basic set operations +* add item into a set +* compare set and dictionary +* identify when dictionary or set is appropriate in a problem diff --git a/_Mini_Projects/background-web.md b/_Mini_Projects/background-web.md deleted file mode 100644 index 5652298..0000000 --- a/_Mini_Projects/background-web.md +++ /dev/null @@ -1,146 +0,0 @@ ---- -title: Web Basics -permalink: /mini_projects/background-web -key: miniprojects-background -layout: article -license: false -sidebar: - nav: MiniProjects -aside: - toc: true -show_edit_on_github: false -show_date: false ---- - -This document should provide you with the basic idea on how to setup a basic web server and its interaction with the web browser. It will **not** make you a web developer, but at least you may understand some fundamentals for the mini project. - -Disclaimer: note that everything described here is a **gross** oversimplification of what actually happens in your computer. Take the subject **50.005** in ISTD (Term 5) if you'd like to learn more. -{:.error} - -## How the Web Works (Baby Edition) - -When you type in a URL (a.k.a web address) in your web browser's search bar, you can think of your browser as sending a **request** message out to the internet to reach the recepient with that address matching the URL you just typed. For example, type: `http://natalieagus.net:1234` in your web browser (you need to allow unsecure connection), and you will be faced with this output: - - - -### Inspecting a Site - -This is a _website_, an **overtly simple** website containing just a **single** text: `My first server!`. Where did your browser get this particular information? Before we go there, let's see what "this" information is. Right click on your browser and click **inspect** (you might need to [enable **developer tools**](https://support.apple.com/en-sg/guide/safari/sfri20948/mac) if you use Safari): - - - -### Looking at Sources - -Under `Sources` tab you should see that there's only **one** file called `index` that's sent by `natalieagus.net:1234`. Inside that file we can find a text `My first server!` and **nothing else**. No color, no styling, no images, no videos, no fancy stuffs that you will find in a modern website. Whereas if you load our course website and **inspect**, under Sources tab you will see a lot more files being sent over by `https://data-driven-world.github.io`: - - - -All these files: `.js, .css, .html` are **processed** and **rendered** by our browser so that you can see what you currently see on your browser page. - -## Web Server - -The big question now is: **who** sent these files over to our browser? **Who** is this "entity" that answered our "request" when we type in the URL in the search bar, and _then_ reply with these bunch of files for our browser to render and eventually for us to read? - -This "entity" is called a web server. Just like a regular restaurant _server_, the web server's job is to **give** (serve) relevant files when **requested**. -{:.info} - -A web server is an application (just like any regular application in your computer such as your Elden Ring, Telegram, Web Browser, VSCode, etc) and it _typically_ does not have a graphical user interface. It has **one main job**, to reply to website-related requests directed to it. The "internet" is just a generic name of various **infrastructures** to make it possible for your computer to **communicate** (send "packets" of data) with other computers (servers) around the world so that you can load your Netflix series and play Valorant. - -> You can think of the Internet as a bunch of _roads_ (medium) made for these "packets" of data to "travel". - - - -### Physical Location - -So **where** is the Web Server for `http://natalieagus.net:1234`? - -By _where_, we mean _where_ is the computer running the web server to answer requests by browsers accessing `http://natalieagus.net:1234` located? Well, the web server is run on AWS EC2, so the **actual** device running that piece of server program can be [any of these AWS server locations](https://aws.amazon.com/about-aws/global-infrastructure/). - -What about the location of the computer running the web server for `https://data-driven-world.github.io`? - -We **don't know**. Github does not exactly advertise its server locations for security reasons, etc. It could be in the US, it could be in the EU, or it could be right here in Singapore. The beauty is that **we don't have to care**. We focus on making a nice website and design how users can interact with it, then engage companies like GitHub or Amazon to **host** (run the program) of our web server. - -### Local Web Server - -When you type in the command `flask run` for the mini projects, you are essentially spawning a **web server** in your own computer. That is why you can access your website by typing the URL `http://127.0.0.1:5000/` in your web browser. The value `127.0.0.1` means **yourself** (your own addresss), so your browser will send a **request packet** addressed to yourself, which will arrive at the python web server you are currently running. It will then reply with the necessary files for your browser to render the MP1 welcome page: - - - -## Hello Flask! - -It is useful to try to create your very basic own web server in Python with `flask` before going further into the MP. - -Flask is a Python **web framework**. It is a tool that you can use so that it is easy to make and deploy a website. It abstracts away the **need to know how** to: - -1. Process incoming request from the web browser -2. Craft the **correct** "response" to the web browser -3. Run a web server in **port** `5000` (just think of this like buying a house and getting a unit number so people can reach you) -4. Other unpleasant detail on how the web works, optimisation, etc, making developing and maintaining website so much easier - -There are **plenty of web frameworks** out there: [Ruby on Rails](https://rubyonrails.org/), [Angular](https://angular.io/), [React](https://reactjs.org/), and [Svelte](https://svelte.dev) to name a few. You need to know that not all web frameworks are the same. Some of them are **full stack**, some **front-end**, and some are **micro-framework**. Flask is micro-framework (simple, does not require any pre-existing third-library parties to provide common functionalities). - -Create a new folder named `flaskexample` in a **path** of your choice, and "open" that folder in your terminal by `cd`-ing to it. Then, you can run the command `pipenv install flask` to install it to the `flaskexample` (matching the folder name) virtual environment. - - - -Now create a new file called `app.py` inside `flaskexample` folder with the following content: - -```python -from flask import Flask - -app = Flask(__name__) - -@app.route('/') -def index(): - return 'My first server!' - -app.run(host='0.0.0.0', port=81) -``` - -The file `app.py` **must** be in this name. `flask` looks for `app` folder or `app.py` file as an **entry point**. -{:.warning} - -This will prompt you to open `http://127.0.0.1:5000/` on your browser, and you will be met with the message `My first server!` which is what's returned by the `index()` function above. - - - -The mini project is **more complicated** than just sending a text back to your browser. There are several **routes** (like different **paths**) in the MP; that is if you add a slash `/[path]` at the back of the URL, the `flask` app knows which files to **serve** (send back) to your browser. Looking at routes.py, this should be **obvious**: - -```python -from flask import render_template -from app import application - -@application.route('/') -@application.route('/index') -def index(): - return render_template('index.html', title='Mini Project 1 Home') - -@application.route('/ex1') -def exercise1(): - return render_template('ex1.html', title='Mini Project 1 Exercise 1') - -@application.route('/ex2') -def exercise2(): - return render_template('ex2.html', title='Mini Project 1 Exercise 2') -``` - -1. If we type `http://127.0.0.1:5000/` (homepage) in the browser, this server send `index.html` file back to the browser -2. Else if we type `http://127.0.0.1:5000/ex1` (homepage) in the browser, this server send `ex1.html` file back to the browser -3. Else if we type `http://127.0.0.1:5000/ex2` (homepage) in the browser, this server send `ex2.html` file back to the browser -4. Else, a generic URL not found message will be sent back to the browser (automatically handled by Flask unless you override) - - Try typing `http://127.0.0.1:5000/ex3` and see what happens - -## Conclusion - -Notice how whenever you request the URL, the console on the web server app will print out soemthing about `GET/ HTTP/1.1`. That is the "request" message that's sent by our browser to our web server. `HTTP` is a [well-established protocol](https://developer.mozilla.org/en-US/docs/Web/HTTP) for transmitting web-related documents such as the HTML file. - -```bash - * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit) -127.0.0.1 - - [08/Sep/2022 13:18:09] "GET / HTTP/1.1" 200 - -``` - -We don't need to know or care how `HTTP` works to send the `My first server!` reply. This is the **magic** of web frameworks like Flask, it **abstracts away** basic details so that we can **focus** on making your website work. Note that advanced understanding and skill in **web development** requires you to possess a **full stack knowledge**, starting from how your computer works, how operating system runs various programs and manage resources (CPU, RAM, Cache, etc), how the internet works, the network protocol stack, network and system security, **on top of** getting up to date with the most recent web development frameworks and **mastery** in programming skills (Javascript, Flutter, Kotlin, etc), **and** possibly knowing how to **test, maintain, and scale** your project (the DevOps department, buzzword: CI/CD). It can take easily a **decade** to do all these, so get your **fundamentals right** and take it easy should you ever want to dive into the software engineering world. - -### Where to go from here? - -If you have not tried any other web development framework before, you can give Flask a try. There are plenty of amazing online tutorials about Flask out there [like this one](https://www.youtube.com/watch?v=mqhxxeeTbu0&list=PLzMcBGfZo4-n4vJJybUVV3Un_NFS5EOgX). If you want something else fancier then you can give [Svelte](https://www.youtube.com/watch?v=zojEMeQGGHs&list=PL4cUxeGkcC9hlbrVO_2QFVqVPhlZmz7tO) a try. diff --git a/_Mini_Projects/background.md b/_Mini_Projects/background.md deleted file mode 100644 index 3c8fde1..0000000 --- a/_Mini_Projects/background.md +++ /dev/null @@ -1,343 +0,0 @@ ---- -title: Command Line Basics -permalink: /mini_projects/background-cli -key: miniprojects-background -layout: article -license: false -sidebar: - nav: MiniProjects -aside: - toc: true -show_edit_on_github: false -show_date: false ---- - -This document shall provide you with sufficient knowledge to understand why you type every single command to get the mini projects running. It is **not** technically in-depth (you'd have to go to ISTD for that), but is _sufficient_ to get you through the mini projects **setup**. - -Disclaimer: note that everything described here is a **gross** oversimplification of what actually happens in your computer. Take the subject **50.005** in ISTD (Term 5) if you'd like to learn more. -{:.error} - -## Interacting with Your Computer - -Most of you have been using your computer daily by interacting with its Desktop **graphical user interface**: - -- Clicking the start menu -- Double-clicking an app to open -- Right-clicking ---> select options to rename/delete files -- Clicking options in the toolbar, etc - -You customise your desktop: wallpaper, shortcuts, files, screensaver --- forming a **desktop environment** that you can interact with _graphically_ (e.g: move mouse and click). - -A desktop environment typically consists of icons, windows, toolbars, folders, wallpapers and desktop widgets. -{:.info} -
- -Long before Desktop GUI is made, people interact with their computers via the the **Command Line Interface** (CLI, also known as the termina). They enter **commands** via text to use the computer entirely. For example, suppose we want to rename the folder `Data-Driven-World.github.io` to `Data-Driven-World-website.github.io`. You might think that **renaming** a file can only be done by right clicking on the file and then ---> `rename`. - - - -However, one can enter the following **command** to the terminal and achieve the same result: - -``` -mv Data-Driven-World.github.io Data-Driven-World-website.github.io -``` - -## The Command Line Interface - -As said above, the command-line interface (CLI) is a **text**-based user interface (UI) used to **run** programs, **manage** computer files and **interact** with the computer. In fact, almost _everything_ (OS-related stuffs) can be done via the CLI. - -> Who needs desktop ๐Ÿคท๐Ÿปโ€โ™€๏ธ - - - -Each operating system has its own command line: - -- **Windows**: Command prompt / Powershell / [Terminal](https://docs.microsoft.com/en-us/windows/terminal/install) - - We recommend using Terminal as it tries to maintain compatibility with Unix commands -- **macOS**: Terminal, iTerm2 -- **Linux**: We believe in you โ˜บ๏ธ - -### Getting Started - -Open a terminal window. Regardless of whichever OS you use, you are likely to be faced with a **prompt** (the thing with the cursor, waiting for your `command`). - - - -You can type `commands` into the prompt, and then press `enter` to **execute** that command. For example, the first two commands you have to enter to **download** the `mp_sort` (mini project 1) repository is: - -``` -cd Downloads -git clone https://github.com/Data-Driven-World/d2w_mini_projects -``` - -Each of the line above is **one** command. The first command is **cd** (stands for change directory). It changes your **current working directory**, just like how you click open folders after folders in your Finder or File Explorer to navigate through your **file system** and find the right location and create new things in this location you want: - - - -The same thing can be done via the command line: - - -So `cd Downloads` is none other than **navigating** to `~/Downloads` folder. - -The next thing to do is to **download** the starter code for your mini projects. The second command `git clone [project_url]` does that. We ask the program **git** to `clone` (**download**) the repository situated in the url into your `~/Downloads` folder. . This is essentially the same as actually opening the url on the browser, and clicking **Download ZIP**, landing the project in your `~/Downloads` folder.: - - -Notice how the new folder `d2w_mini_projects` are created after you `clone`: - - -### How CLI Works (Baby Edition) - -Remember that previously you have [installed](https://git-scm.com/download/mac)`git`? Installation means that you downloaded the **program** to your computer so that your computer can **execute** (use) it. This **git** program has many **functionalities**, just like how another program you are familiar with: Microsoft Word have their own **functionalities**. The difference between **git** and Microsoft Word is that the latter has a **graphical user interface** (you can see the windows, buttons, etc and click), while the former only has a **command line interface** (it accepts textual-based commands). - -The horror with CLI-only programs is that you might need to **memorise** a few useful inputs to the program _prior_ (which unlike Microsoft Word interface, the GUI is quite intuitive). This can be done by reading git [documentation](https://git-scm.com/doc) or simply googling terms like: _common **git** commands_, etc. - -Notice that **git** (the first word of the command you typed above) is actually a **program**, and that `clone [project_url]` is an **input** to the **git** program; it means that we tell git to `clone` from this url. **git** accepts various inputs: `git commit -m [message]`, `git init`, `git add [files]`, and [many more](https://git-scm.com/book/en/v2/Git-Basics-Getting-a-Git-Repository). - -#### Where is git? - -We can find git by typing `whereis git` command (for macOS and Linux only), or `where git` command for Windows. It will print the **path** of where this `git` program is stored. In the example below, its stored in `/usr/bin/git`. We can actually find it via the file finder, and attempt to **double click** to open it. - -- It will open only a terminal, and print out some output before terminating -- This is because simply **double clicking** git does not gives it adequate **input** (like `clone`) -- This is equivalent to just typing the command `git` and pressing enter in the terminal as shown - - - -As you can see, `git` is just like any other programs you have downloaded and installed in your computer (MS Word, Steam, Telegram, Whatsapp), just that these programs have a **graphical user interface** while `git` does not. - -#### Where is python? - -`python` works the same way. You can find where `python` is installed in your computer and try to **double click** it. In the demo below, python in installed in `/Users/natalie_agus/.pipenv/shims/python`. Finding it via the file finder and double-clicking it opens a terminal window where you can use python interactively in the terminal. - - - -When you want to run a Python script, you can use the command `python file.py`, which means to give `file.py` as an **input** to the `python` **program** and **run** it. - -#### Summary - -The **first word** of each command that you have to enter to the CLI is **most likely** the **name** of the program that you want to execute. Whatever that comes on the right side of that program name is the **input** to that program. -{:.info} - -A special exception is `cd` (this is not a program, go and take ISTD subject 50.005 to find out more), but all other commands you will use for `mp_sort` is a program. Try to find where the following resides (the path) in your computer and open it via the GUI file finder: - -1. `ls` or `dir` -2. `flask` -3. `pipenv` -4. `pip` - -## File Path - -Each file on a computer has a **path**, such as `C:\Users\natalie_agus\Downloads\d2w_mini_projects\mp_sort\application.py` or `/Users/natalie_agus/Downloads/d2w_mini_projects/mp_sort/application.py`. Usually we can shorten it into `~/Downloads/example-file.txt` where `~` is `/Users/natalie_agus`, also known as your **home** path. - -The reason one uses `/` (macOS/Linux) and the other `\` (Windows) is because each OS uses a **different file system**. Think of it like a different manager and storage system. -{:.info} - -Folders also have a **path**. The path of the `Downloads` folder is then `C:\Users\natalie_agus\Downloads` or `/Users/natalie_agus/Downloads`. It is important for your terminal to "open" the right folder before executing a command, otherwise you **may not find the file**. You can find out your terminal's current "opened" folder using the `pwd` command (macOS/Linux) or `cd` (without any parameter) for Windows. - -For example, if we execute the command `python application.py`, we need to **ensure** that the current working directory of the CLI is at `/Users/natalie_agus/Downloads/d2w_mini_projects/mp_sort`. The example below shows a scenario where you are at the **wrong** directory, and the file `application.py` is **not found** (error message printed _by_ the terminal). - - - -We need to "open" the `mp_sort` folder first before we can successfully launch `application.py`: - - - -Notice the error message is no longer `[Errno 2] No such file or directory`, but `ModuleNotFoundError: No module named 'flask'`. We know two things from this message: - -1. `python` has launched **succesfully**, and the error message above is printed by `python` and not our terminal -2. We have **not** installed `flask` module for `python` to **import** - -### Path Navigation in CLI - -You can use `cd [path]` to navigate ("open") the folder that you want in the terminal. - -For instance, `cd /Users/natalie_agus/Downloads/d2w_mini_projects/mp_sort` opens the `mp_sort` folder right away (that is if you can **remember** its path). Usually, people can't remember the path of their files, and instead perform `cd` in **stages**, combined with `ls` or `dir` to **view** the list of files in the current opened folder: - - - -Tips: press `tab` to **autocomplete** certain commands. The example you saw above utilises many terminal **extensions** to make your terminal **pretty**. macOS or Linux users, do yourself a favor and read [this article](https://medium.com/@shivam1/make-your-terminal-beautiful-and-fast-with-zsh-shell-and-powerlevel10k-6484461c6efb). -{:.info} - -## **PATH** Environment Variable - -Not to be confused with file **path** concept above. -{:.error} - -`PATH` is simply a **variable** containing a **list** of directories to search when you enter a command into the command line. Formally, it is called **environment variable** but you don't have to understand what it means for now (take 50.005 in ISTD to find out more). This is the magic behind command execution. - -You can check this list by typing `echo $PATH` in your macOS/Linux terminal, or `$Env:Path` in your Windows terminal - -- You will have an output looking as such -- Each "value" is separated by the **colon** (`:`) - - -For example, if you enter `python` into the terminal, the terminal does the following: - -- Goes through the **list** of directories in the `PATH` variable, - - It will start searching for `python` in `/Users/natalie_agus/.pipenv/shims` if the `PATH` content is as the screenshot above -- Checks each directory if it contains the `python` executable -- If it does, **execute** it -- Else, check the next directory - - If `python` doesn't exist in `/Users/natalie_agus/.pipenv/shims`, it will look for `python` in the second value: `/Users/natalie_agus/.rbenv/shims`, and so on. -- If `python` is not found anywhere, it will print `command python not found` and returns - -### **Adding Executables to your PATH** - -If you've ever tried to execute a command and was faced with an error such as the following in Windows: - -```shell -[name] : The term '[name]' is not recognized as the name of a cmdlet, function, script file, or operable -program. Check the spelling of the name, or if a path was included, verify that the path is correct and try -again. -At line:1 char:1 -+ xxx -+ ~~~~~~~ - + CategoryInfo : ObjectNotFound: ([name]:String) [], CommandNotFoundException - + FullyQualifiedErrorId : CommandNotFoundException -``` - -or the following in macOS/Linux: - -```shell -zsh/bash: command not found: [name] -``` - -The above is simply an indication that your computer has searched through your `PATH` variable, but has not managed to find the **program** whose **name** **matches** the requested command. -{:.info} - -To **fix** this, we simply need to ensure that our computer can **find** this executable. Windows handles this differently from macOS/Linux, so we describe the process for both operating system types: - -### **Windows** - -1. Search for '**Environment Variable**' into the Windows search bar, and click on '**Edit the system environment variables**' -2. Click on the '**Advanced**' tab -3. At the bottom of the tab, click on '**Environment Variables**' -4. On the new '**Environment Variables**' window that pops up, look for the '**PATH**' entry in the list of '**System variables**' (bottom half of the window) -5. Select the '**PATH**' entry, and click '**Edit...**' -6. You can now add a new directory by clicking '**New**' and entering the desired path (the location where you want the terminal to find the program whose name matches the first word of the command you will enter in the CLI) -7. When finished, click '**OK**' on all the windows -8. **Make sure to close and re-open a new terminal window before checking if the executable was successfully added to your `PATH`** - -You can refer to [various online guides](https://docs.oracle.com/en/database/oracle/machine-learning/oml4r/1.5.1/oread/creating-and-modifying-environment-variables-on-windows.html) if you're stuck. - -### **macOS and Linux** - -We can add to our $PATH by editing our `~/.bashrc` or `~/.zshrc` file, which can be thought of as a **configuration** file for our terminal. This file can be found in your **home** directory. - -- If your terminal uses `zsh`, you should edit `~/.zshrc` file -- else, you might be using `bash`, and you should edit `~/.bashrc` file -- If you're using other stuffs like `fish`, `tcsh`, or `ksh`, you're beyond us and you're probably not reading this document ๐Ÿ˜„ - -1. Navigate to your home directory in your command line - - ```shell - $ cd ~ - ``` - -2. Open the `.bashrc` (or `.zshrc`) file with the text editor of your choice, for example `nano`: - - ```shell - nano .bashrc - ``` - -3. Scroll to the bottom of the file, and add the following line: - - ``` - export PATH=/path/to/exec:$PATH - ``` - -4. Save the file (`Ctrl-O` then `Return` then `Ctrl-X` in `nano`) - > Please edit `/path/to/exec` to point to the directory where your executable is located, and **do not just blindly copy-and-paste** - -To explain the line we just added to the `.bashrc` file: - -- `export = ` **sets** a variable to the specified value - - Hence we are setting `PATH` to a new value -- `/path/to/exec:$PATH` has the value of our **old** PATH variable appended at the back, plus our desired new directory - - `$PATH` has a dollar sign because we want to reference the **contents** of the `PATH` vairable - - `:` is the separator used for the PATH variable - -## Python Modules - -When you navigate to wherever `mp_sort` is, e.g: `cd /Users/natalie_agus/Downloads/d2w_mini_projects/mp_sort`, and try running `python3 application.py` before anything else, you might be met with **ModuleNotFoundError**, namely that you might not have `flask` installed. - -The program `pipenv` helps you install and **manage python modules** per project (libraries, which is just scripts that can be used by you as tools to do things). One popular module is `numpy`, which contains many matrix-related functions (dot product, cross product, etc). For this mini project, we will be using a bunch of python modules that we need to install. It is listed inside `requirements.txt` inside `mp_sort`: - - - -### pipenv - -The first thing to do is to install `pipenv` to your computer using the command: - -``` -pip install --user pipenv -``` - -> `pip` is a program that is _installed_ (placed in the `PATH`) when you installed Python to your computer. It helps you install another program called `pipenv`. - -If you're met with such **WARNING**, add the path to your `PATH` variable: - - -For instance, we add the path `/Users/natalie_agus/.local/bin` to our `.zshrc`: - -```bash -export PATH="$HOME/.local/bin:$PATH" # pipenv -``` - -Afterwards we can use `pipenv` to **install** all modules listed in `requirements.txt`. - -### Where are these modules installed? - -You can find the path where `pipenv` install your modules for **this project** by typing the command: - -```bash -pipenv --venv -``` - -It will return a path **specific to this project**, for example: - -```bash -/Users/natalie_agus/.local/share/virtualenvs/mp_sort-Xom1cKhU -``` - -If you open that path in your GUI File Finder, you will find all the **modules** (both scripts and executable): transcrypt, flask, wheel, etc in that location. Watch the gif below to understand more: - - - -### pipenv shell - -You still can't run `python application.py` before running `pipenv shell`. - -This is because despite these modules being **present** somewhere in your computer, `python` **cannot** find it. You need to **activate** the environment by typing the command `pipenv shell`, which is none other than telling the terminal to **look** for python-related modules in the path: `/Users/natalie_agus/.local/share/virtualenvs/mp_sort-Xom1cKhU`. - -### Summary - -When you work on **various projects**, they will require **various modules**. You want to be **more organised** and have a separated **environment** (a dedicated place where modules for each individual project is stored). This is what `pipenv` is for. -{:.info} - -Note that you can install any module without `pipenv`, with the command: - -```bash -pip install [module-name] -``` - -This will install your modules in a **standard** path such as: - -``` -/Users/natalie_agus/.local/lib/python[version]/site-packages -``` - -where `python[version]` can be your default python version, e.g: `python3.10`. You can imagine how that `site-packages` folder is going to be (very full!) when you install modules for all projects you ever touch into that folder. - -## Conclusion - -We have introduced to you **how command works**, and that the first word of a command is none other than the _name_ of the program you'd like to execute (well, at least for now). The commands that have been introduced to you for this mini project are: - -1. `cd`: change directory -2. `git` (clone) -3. `pwd` for macOS/Linux or `echo %cd%` for Windows: check current working directory (check currently "opened" folder) -4. `ls` for macOS/Linux or `dir` for Windows: print out the files/folders in the currently "opened" folder -5. `pip install ...`: run pip to install necessary modules for your project, like `flask` -6. `python ....`, `flask....`: run `pip` or `python` for your project - -> Notice that `flask run` and `python application.py` does the same thing, that is to run the script `application.py`. diff --git a/_Mini_Projects/checkoff.md b/_Mini_Projects/checkoff.md deleted file mode 100644 index 6486433..0000000 --- a/_Mini_Projects/checkoff.md +++ /dev/null @@ -1,119 +0,0 @@ ---- -title: Mini Project Checkoff Protocol -permalink: /mini_projects/checkoff -key: miniprojects-checkoff -layout: article -license: false -sidebar: - nav: MiniProjects -aside: - toc: true -show_edit_on_github: false -show_date: false ---- - -## General Checkoff Protocol - -To obtain **all 5 pts** of MP marks, you need to **submit** your project to Vocareum AND complete the in-person **checkoff**. Please approach **your cohort's TA** or your cohort's **instructor** during Mini Project session of the week for checkoff. - -Ensure that you have submitted the code to Vocareum and **done** with the checkoff this **BEFORE the deadline** (refer to your course handout and Vocareum). -{:.info} - -There are 3 criterias for the grading as reflected in Vocareum. They make up the 5pts: - -1. **[2pt]** Working Test Case -2. **[2pt]** Understanding of Code -3. **[1pt]** Code Quality - -Before you approach them for checkoff, you need to ensure that all components above are fulfilled: - -1. **[1pt]** **Code Quality**: Ensure that your project can **compile** and MUST run in Vocareum. Running locally **does not count** (will lose this point). We will scan your code for **quality** (consistent variable naming, clear comments). You [can read this article](https://testdriven.io/blog/clean-code-python/) (take it lightly, we won't be so strict) for starters. -2. **[2pts]** **Working Test Case**: Ensure that the items in the **checklist** (see respective heading below) are **functioning**. - - If any checklist item is missing, your grade will be prorated accordingly -3. **[2pts]** **Understanding of Code** via **Q&A**: we will verbally ask 3 **related questions** to **any** student in the team. There are two question categories, at least 1 question from each category will be selected. You're required to score 2 out of 3 questions accurately to obtain fullmarks. Simply open your related project files during the checkoff and give a verbal explanation. - 1. About your implementation (exercise related) - 2. About the project in **general** (background knowledge) - -Our instructor/TA will then record the points for your MP in our internal excel sheet (and/or Vocareum). We will release it after the MP deadline. - -Please bring your laptop(s) and open your project when you approach our TA/Instructor for checkoff. **We want to see your program in person.** -{:.info} - -## Q&A Grading Breakdown - -The Q&A session is **important** and will fulfil the **Understanding of Code** criteria. It really tests the understanding of your implementation and your general knowledge about the project. You may get various marks between **0 to 1 pt** per exercise with granularity of **0.25pt** depending on your answer. - -> Total max mark for this section is 2pts. 1pt max for **each** exercise in the MP. - -Here's the general rubric for the Q&A section **for each exercise**: - -| Points | Explanation | -| --------- | ----------------------------------------------------------------------------------------------------------------------------------- | -| ๐Ÿ˜„`1` | Can answer **2 out of 3** with clarity (googling, internal discussion, reference to old notes allowed) | -| ๐Ÿ™‚`0.75` | Can answer **2 out of 3** but with **hints from TA/instructor**, and takes a comparably long time (lots of staring, silent moments) | -| ๐Ÿ˜`0.5` | Only can answer 1 question accurately, the rest are _smokey_ | -| ๐Ÿ˜Ÿ`0.25 ` | Cannot answer any question accurately, but at least know **which file** contain their answer, and some explanation | -| ๐Ÿ˜ตโ€๐Ÿ’ซ`0` | **No idea** at all about the project, not even about admin matters like which file should contain their answer | - -Note that we might ask any of you for answers. Please ensure that everybody knows all parts of the project, at least the big picture. This is where you will learn. -{:.info} - -## Mini Project 1 Details - -There are **8 checks** for both exercises in MP1. Each is worth **0.25 pts**, totalling of **2pts** under "Working Test Case" **criteria**. - -### Exercise 1 [4 checks] - -1. 10 **integers** (OK to have repeated value) appear when `Generate 10 numbers` button is clicked -2. Totally sorted **integer** output is displayed when `Sort` button is clicked -3. In `/app/static/library.py`, the implementation in `sortnumber1()` is **NOT hardcoded** like this, but some sorting function is called to actually compute the sorted value. -4. A **custom** sort function (e.g: `bubble_sort` with actual implementation) is used to compute (2), and not using python's default [`list.sort()`](https://docs.python.org/3/howto/sorting.html) - -### Exercise 2 [4 checks] - -1. The textbox can accept **integers**, separated by a comma. **NO NEED** to test for float, string, or other weird data types (but it is good practice, just that we aren't so strict for this checkoff) -2. Some kind of **warning** should appear when the textbox is empty but `Sort` is clicked (no error) -3. The output **integers** are sorted properly when the button `Sort` is clicked. -4. A **custom** sort function (e.g: `bubble_sort` with actual implementation) is used to compute (2), and not using python's default [`list.sort()`](https://docs.python.org/3/howto/sorting.html) - -### Q&A - -Exercise related question involves questioning **how** and **why** you implement certain things in your project. - -> It is pretty clear cut what it entails: we will ask about any instruction (program) that **you** wrote for this project (anything that you wrote is fair game). Examples include your sort implementation, how did you display the values to the webpage, how did you manipulate certain values, etc. - -Project-related question involves **understanding** the project structure: - -1. Command-line related question (all commands that we ask you to type for MP1 is fair game) -2. Flask project structure brief overview (what **each** file is for?) -3. Web Server and Browser Client communication (very basic idea) -4. General understanding of Python modules used in the project -5. General understanding of the Python virtual environment - -Don't worry, it will be very **basic**. If in doubt, please read [this handout about CLI background](https://data-driven-world.github.io/mini_projects/background-cli) and [this handout about the Web](https://data-driven-world.github.io/mini_projects/background-web) that we have asked you to read in Week 1. - -**Why do you have to know all these: CLI/Web/structure of project as well?** - -It is important to be **independent** and **curious** about things you've worked on, so that you can **apply** this knowledge in other circumstances and not just studying for the sake of the MP only. Doing things for one-off purposes are such a waste of time. And of course, for those of you who are going to ISTD, it is **crucial** to have this mindset and not to mention the knowledge about CLI and the Web. The material provided in the background handouts are **nothing** compared to what you will face in Term 4 and Term 5 as **most details** are omitted, but they will give you about 2% heads up ๐Ÿฅน. - -## Mini Project 2 Details - -There are **8 checks** for both exercises in MP1. Each is worth 0.25 pts, totalling of 2pts under โ€œWorking Test Caseโ€ criteria. **This is similar to MP1**. - -### Exercise 1 [3 checks] - -1. In `/app/serverlibrary.py`, there exist a `mergesort` implementation **from scratch**, no other sorting libraries can be used. -2. Able to **create** users **and** login as that user. -3. Able to **create** many users and display all registered users under `Users` page. - -### Exercise 2 [5 checks] - -1. In `/app/serverlibrary.py`, there exist a `Stack` implementation in there **from scratch**, e.g: `pop`, `peek`, etc is implemented/ -2. In `/app/serverlibrary.py`, there exist a `EvaluateExpression` implementation in there **from scratch**. -3. Users can create questions and the answer displayed in `/questions` page is correct. Users can also send it to multiple other users. -4. Users can `Show/Hide` questions and enter the correct answer in the `/challenges` page, afterwhich the timer value is displayed at the row. -5. In the **Hall of Fame**, we can see the **ranks** properly, where each ROW is a question, and WITHIN the same row of question, the display of username and score must be shown properly (top 3)/ - -### Q&A - -Similar protocol like MP [above](http://127.0.0.1:4000/mini_projects/checkoff#qa). Please read all necessary markdown files: [Bootstrap](https://github.com/Data-Driven-World/d2w_mini_projects/blob/master/mp_calc/Bootstrap.md), [Database](https://github.com/Data-Driven-World/d2w_mini_projects/blob/master/mp_calc/Database.md), and [Forms](https://github.com/Data-Driven-World/d2w_mini_projects/blob/master/mp_calc/Forms.md) that was available at the [mini project 2 repository](https://github.com/Data-Driven-World/d2w_mini_projects/tree/master/mp_calc) before going for the checkoff. diff --git a/_Mini_Projects/debug-notes.md b/_Mini_Projects/debug-notes.md deleted file mode 100644 index f43b1ee..0000000 --- a/_Mini_Projects/debug-notes.md +++ /dev/null @@ -1,104 +0,0 @@ ---- -title: Debug Notes -permalink: /mini_projects/debug-notes -key: miniprojects-debug -layout: article -license: false -sidebar: - nav: MiniProjects -aside: - toc: true -show_edit_on_github: false -show_date: false ---- - -This notes compile all sorts of bugs that you _might_ encounter when running the mini project. Hopefully this helps ๐Ÿฉน. - -### TLDR: Running on Vocareum - -We have given the steps to you in the `README` file, we have also written additional explanations for you. But as the cherry on top, here's the steps: - -We assume you follow the _easy step_, which is to clone the original repository and just paste your answer. -{:.info} - -1. **Clone** the repository - ```shell - git clone https://github.com/Data-Driven-World/d2w_mini_projects.git - ``` -2. **Refresh** the webpage, you should see it in vocareum's file tree on the left hand side pane -3. Change directory, **install** pipenv, **install** modules, **start** pipenv shell - ```shell - cd d2w_min_projects/mp_sort - pip install --user pipenv - pipenv install - pipenv shell - ``` -4. Change the **content** of `/app/__init.py__`: set `voc=True`. You can click the python file from the Vocareum left pane. - ```python - # set voc=False if you run on local computer - application.wsgi_app = PrefixMiddleware(application.wsgi_app, voc=True) - ``` -5. Paste your answer in the **relevant files**, e.g `library.py`, `*.html` files, etc. -6. Run **transcrypt** (assuming your current directory is `mp_*`) - ```shell - cd /app/static - python -m transcrypt -b -n library - ``` -7. Go back to `mp_*` directory, change the bash script to be executable and run: - ```shell - cd ../.. - chmod a+x ./runflaskvoc.sh - ./runflaskvoc.sh - ``` -8. Once it is running, you can open another tab in your browser and type the following url: [https://myserver.vocareum.com/](https://myserver.vocareum.com/) - -### Env does not have the var VOC_PROXY_ID - -You need to add the **trailing slash** at the URL as shown in the screenshot below: - - -### bash: ./runflaskvoc.sh /bin/bash^M: bad interpreter - -This is due to the way **newline** is encoded in Windows vs UNIX machines. You can read more about it [here](https://support.nesi.org.nz/hc/en-gb/articles/218032857-Converting-from-Windows-style-to-UNIX-style-line-endings). - -#### Fix - -- Open runflaskvoc.sh on vocareum -- Go to line 2 (the empty line after #!/bin/bash) -- Press backspace -- Press enter -- Wait for it to save -- Run ./runflaskvoc.sh again - -> Courtesy of TA Daniel - -### Error: The server responded with a non-Javascript MIME type of "text/plain" - -It means that _something_ might have changed your [Windows registry](https://support.microsoft.com/en-us/windows/how-to-open-registry-editor-in-windows-10-deab38e6-91d6-e0aa-4b7c-8878d9e07b11) file. - -#### Fix: - -- Open your search bar by pressing Win + R -- Type in regedit and press enter -- Find .js under the parent path HKEY_CLASSES_ROOT -- Change the data field of Content Type from text/plain to application/javascript -- Save, and then re-run flask run - -> Courtesy of TA Alex - -### IndexError: list index out of range - -If the error as such appear after typing the `transcrypt` command: - -```shell -Traceback (most recent call last): - File "/mnt/vocwork2/ddd_v1_w_2bG_1401946/asn1029778_3/asn1029779_1/work/.local/share/virtualenvs/mp_sort-K9CB8Yy2/lib/python3.8/site-packages/transcrypt/__main__.py", line 162, in main - compiler.Program (transpilationDirs, __symbols__, __envir__) - File "/mnt/vocwork2/ddd_v1_w_2bG_1401946/asn1029778_3/asn1029779_1/work/.local/share/virtualenvs/mp_sort-K9CB8Yy2/lib/python3.8/site-packages/transcrypt/modules/org/transcrypt/compiler.py", line 112, in __init__ - message = f'\n\t{exception}' - File "/mnt/vocwork2/ddd_v1_w_2bG_1401946/asn1029778_3/asn1029779_1/work/.local/share/virtualenvs/mp_sort-K9CB8Yy2/lib/python3.8/site-packages/transcrypt/modules/org/transcrypt/utils.py", line 215, in __str__ - result += '\n\tFile \'{}\', line {}, namely:'.format (str (program.importStack [-1][0] .name), self.lineNr) -IndexError: list index out of range -``` - -then it is likely that you **did not execute transcrypt** in `app/static` directory. Simply `cd` there and re-run the `transcrypt` command again. diff --git a/_Mini_Projects/overview.md b/_Mini_Projects/overview.md deleted file mode 100644 index 58669e9..0000000 --- a/_Mini_Projects/overview.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -title: 10.020 DDW Mini Projects -permalink: /mini_projects/overview -key: miniprojects-overview -layout: article -nav_key: miniprojects -sidebar: - nav: MiniProjects -license: false -aside: - toc: true -show_edit_on_github: false -show_date: false ---- - -There are 3 mini projects in this course. Refer to the course calendar in our course notes to find its schedule. - -## Mini Project 1 - -This mini project is to create a simple html pages to generate random number and sort them. - -[Handout](https://github.com/Data-Driven-World/d2w_mini_projects/blob/master/mp_sort/Readme.md) - -## Mini Project 2 - -This mini project is to create a simple web application for math quiz where users can create simple math challenges and take up challenge from others. - -[Handout](https://github.com/Data-Driven-World/d2w_mini_projects/blob/master/mp_calc/Readme.md) - -## (optional) Mini Project 3 - -This mini project is to create a simple AI Tic Tac Toe game where a user can play Tic Tac Toe against a computer. There is no grading on this part and you just need to explore on your own. - -[Handout](https://github.com/Data-Driven-World/d2w_mini_projects/blob/master/mp_tictactoe/Readme.md) - -## Groupings - -Students should work as a pair and practice pair programming in doing the mini project. diff --git a/_Notes/Array_LinkedList.md b/_Notes/Array_LinkedList.md deleted file mode 100644 index 123883d..0000000 --- a/_Notes/Array_LinkedList.md +++ /dev/null @@ -1,207 +0,0 @@ ---- -title: Array and Linked List -permalink: /notes/array_linkedlist -key: notes-array-linkedlist -layout: article -nav_key: Notes -sidebar: - nav: Notes -license: false -aside: - toc: true -show_edit_on_github: false -show_date: false ---- - -In this lesson we will detour a little bit to discuss two kinds of list. One is a Fixed Size Array and the other one is a Linked List. Both are list but they have their differences. - -## Fixed-Size Array - -Python does not have a fixed-size array data type. One of the most basic data type in Python is a `list`. However, many programming languages like C/C++ and Java have this more basic and primitive list-like data type. In these programming languages, you need to declare the size of the array and its type. For example, in C or Java, it will look something like: - -```java -int mynumber[10]; -``` - -Such declaration reserves 10 spaces in the memory to store an array of `int` type. If one `int` number takes 32-bit (32 digits of 0s and 1s), then the program reserves 10 spaces of 32-bit in the memory as shown in the figure below. - -drawing -In the example that we have, `n` is 10 and so the index of the last element is $n-1=9$. You can access element using the index. For example, you can use `mynumber[0]` to access the first element and `mynumber[9]` to access the last element. In C programming language, the name of the array is also the address of the first element. - -Once it is declared to have 10 spaces, the array cannot be extended. You need to reallocate the memory if you have more numbers than what is declared. Moreover, each element has the same data type. The consequence of this is that each element occupies the same size in the memory. Since it occupies the same size, it is easy to know where the other data are. The advantage of this way of creating an array is that it is fast and simple. - -Though Python does not have such fixed-size array, Numpy library implements something similar in its Numpy's array. For example, numpy array must have the same data type. The difference, however, you can extend the numpy array and you need not declare how many elements are there in the array. - -```python -import numpy as np -number1 = np.array([1, 2, 3]) -print(number1, type(number1), number1.dtype) -``` -The output is -```sh -[1 2 3] int64 -``` -The above code shows how to create a numpy array using `np.array()`. It takes in a list as its argument. Numpy will try to detect the data type and in the example above it was detected as `int64` which means a 64-bit integer. - -If one of the array is a float, Numpy will consider all elements as the float. - -```python -number2 = np.array([1, 2, 3.0]) -print(number2, type(number2), number2.dtype) -``` -The output is -```sh -[1. 2. 3.] float64 -``` -As shown in the output, all the elements are printed as a float and the type was detected as `float64`, which is a 64-bit float data type. - -However, Numpy provides additional functionalities to manipulate array such as to append or to insert. - -```python -a = np.array([1, 2, 3]) -b = np.array([4, 5, 6]) -c = np.append(a, b) -print(c) -d = np.insert(c, 0, b) -print(d) -``` -The output is -```sh -[1 2 3 4 5 6] -[4 5 6 1 2 3 4 5 6] -``` - -### List Using Fixed-Sized Array - -In your problem set, you will create your own list based on a fixed-array of Numpy called. You can define a new class, say `ArrayFixedSize` that uses Numpy's array as its internal storage to "simulate" a fixed array in Python. So objects of `ArrayFixedSize` must be declared with its size and its data type. Moreover, you will work on create a list-like data type called `MyArrayList` that is based on `ArrayFixedSize`. In this case, `MyArrayList` works like Python's built-in list where you can append elements to the list. However, since `ArrayFixedSize` has a fixed size, you need to write some codes to create new array with bigger memory size when it is already full. - -### Adding an Element When Array is Full - -Initially, when the list is created, `MyArrayList` will create an empty array with some fixed initial size, say 16 elements. When all the 16 elements are filled up, and a new data is appended, `MyArrayList` will double the size to 32 elements and put the new data at the next empty position. See figure below. - -![](/assets/images/week6/array_add_element.jpg) - -This is one way to work. The disadvantage of this method is that we always have to reserve extra memory space to work. For example, we have 17 elements, we actually reserving 32 spaces where the other 15 positions are empty. The advantage is that since the size is fixed for each element, it is easy to locate the data at any position using its index. Later on, we will consider a different way of creating a list using Linked List. - -### Inserting and Removing an Element - -What can we do to insert an element into a list made with fixed-size array? To insert one element, one can first check if there is enough space in the allocated memory. If there is not enough space, we can double the size of the array as in the case of adding an element at the end. If there is enough space for one more element, then no doubling of memory is needed and we can just shift all the element to the right by one position and insert the element at the position we want it. This is illustrated in the figure below. - -![](/assets/images/week6/array_insert.jpg) - -The above figure shows what happens when we insert the data at position 2 (third position). Assuming that the array is already full, we need to ensure that we have enough capacity to insert a new element. Therefore, we first need to increase the memory size by doubling the array. Once there is enough space, we shift all the elements to the right and modify the value of the element at position 2 (third position). - -What is the computational complexity of such a process? To create a new array with double the size and copy the old values to the new array takes $O(n)$ time. Furthermore, shifting the values by one in the worst case scenario takes $O(n)$ time and modifying the value takes constant time, i.e. $O(1)$. So we should expect such insert operation takes linear $O(n)$ complexity. - -Removing an element is similar. We can simply shift left all the elements by one position. Depending on the design, we may want to choose to keep the empty space available once we reserve it. This, however, may not be a preferred option in systems with small memories like embedded systems. - -## Linked List - -Now, let's take a look at another alternative of creating a list besides using Fixed-size array. Instead of just storing the element, a Linked List stores more information in one **node**. In a linked list, each node contains the following: -- the element -- and the reference to the next element - -The linked list itself stores **references** to two nodes: -- the head of the list -- the tail of the list - -This is shown in the figure below. - -![](/assets/images/week6/linkedlist.jpg) - -Notice that the element itself can be a reference to another object. This arrangement allows several flexibility. First, the element can be objects of different sizes. Since the way to get to some element is through the *next* references, there is no constraint that the element must be of the same size. Moreover, you can add new element as needed by creating a new node and point the tail to the new node and the last element's next reference to this new node. In this way, you need not reserve any empty memory space as in the fixed size array. This arrangement also allows you to have a list with any size without declaring how many elements would be in the list. - -The downside of this arrangement is that it is slower than the fixed size array. In a fixed size array, it is fast and simple to access the element at a particular position using the index since the size of each element is the same. We can get the position of element *i* from: - -$$\text{address_i} = \text{address_0} + i \times \text{size_of_one_element}$$ - -However, with linked list, we have to traverse the nodes to reach the element that we want and this is slower than just computing the exact location. - -### Inserting an Element - -Now, let's discuss the operation of inserting and removing an element from a linked list. We divide such operations into three categories: -- at first position -- at last position -- at position between the first and the last - -Recall that a linked list has references to the first and the last node. To insert a new element at the first position, we do the following: -1. Create a new Node with the new element. -1. Set the first node (i.e the current head) as the *next* reference of the new node. -1. Set the new node as the *head* of the linked list. - -This is shown in the figure below. - -![](/assets/images/week6/linkedlist_insert_first.jpg) - -Inserting at the end of the linked list involves similar process: -1. Create a new Node with the new element. -1. Set the new Node as the *next* reference of the *tail* of the linked list. -1. Set the new Node as the new *tail* of the linked list. - -The only tricky thing is when the linked list is empty. In this case, the *tail* will refer to a NIL. In this case we use the new Node as both the *head* and the *tail* of the linked list. - -If we wish to insert an element in between the first and the last position, we first need to *traverse* the linked list to that particular position. We then do the operation as shown in the figure below. - -![](/assets/images/week6/linkedlist_insert_mid.jpg) - -In the above figure, we insert a new element at position 2 (third element). In order to do so, we do the following: -1. Traverse up to Node 1. -1. Create a new Node. -1. Get the *next* reference of Node 1 and set it as the *next* reference of the newly created Node. -1. Set the *next* reference of Node 1 to point to the newly created Node. - -What is the computational time of inserting an element. The worst case is when we insert a new element to the second last position. In this case, we have to traverse to the node before the tail which takes $O(n-1) \approx O(n)$ time. The other operations takes constant time. Therefore, overall, inserting an element takes linear time, i.e. $O(n)$. - -### Removing an Element - -Removing an element also can be categorized into these three positions: first, last, or in between the first and the last. Let's start for the case when we remove the first element. This is shown in the figure below. - -![](/assets/images/week6/linkedlist_remove_first.jpg) - -In this case, we do the following: -1. Store the head into a temporary node variable -1. We get the *next* reference of the temporary node and set it as the new *head* -1. We can store the element of the temporary node so that we can return it at the end -1. Now, we can delete the temporary node, and -1. return the element of the deleted node - -In the case of removing the last element: -1. we first need to traverse to the node before the *tail*, set this as the current node. -1. Set the current node as the new *tail*. -1. Set the next of the new *tail* to NIL. -1. We can store the element of the deleted node. -1. Delete the node and return the element only. - -Lastly, we need to consider the case when we remove an element which position is in between the first and the last. This is shown in the figure below. - -![](/assets/images/week6/linkedlist_remove_mid.jpg) - -In the above figure, we remove element at position 1 (second position). To do this, we follow the following steps: -1. Traverse the nodes until the node before (i.e. Node 0 in this case) and set it as the current node. -1. Save the next of the current node into a temporary variable. This is the deleted node. -1. Set the next of the deleted node as the next of the current node, i.e. Node 2 as the next of Node 0 in the figure. -1. Delete the node and return the element only. - -Since removing node involves traversing the linked list, the worst case complexity will be linear, i.e. $O(n)$. - -## Base Class for List - -We have discussed two ways of implementing a list and each have its own advantages and disadvantages. For some application, one may choose to use a list based on fixed-size array, while for other applicaiton, one may choose to use a linked list. Both kinds of list, however, can be designed to implement the same operations. This is where our lesson on inheritance can be applied. We can design a base class for our list that is inherited by the two ways of implementing a list. The UML diagram is shown below. - -drawing - -In the UML above, we showed that `MyAbstractList` implements the Abstract Base Class of `Iterator`. To satisfies this, you need to define a method called `__iter__()` in `MyAbstractList` that returns an iterator object. The class `MyAbstractList` also defines some common property and methods for both `MyArrayList` and `MyLinkedList` such as: -- `size`, which is an attribute that stores the number of items in the list. -- `is_empty`, which is a computed property that returns whether the list is empty or not. -- `add(item)`, which adds an item to the end of the list. -- `remove(item)`, which removes an item from the list. -- `__getitem__(index)`, which allows you to use the bracket operator to get an item, e.g. `mylist[index]`. -- `__setitem__(index, value)`, which allows you to use the bracket operator and assignment operator to set a value at a particular index, e.g. `mylist[index] = value`. -- `__delitem__(index)`, which allows you to use the `del` operator and the bracket operator to delete an item, e.g. `del mylist[index]`. -- `__len__(index)`, which is called when you use the `len()` function on the list, e.g. `len(mylist)`. - -This class `MyAbstractList` is inherited by the two classes `MyArrayList` and `MyLinkedList`. The class `MyArrayList` is implemented using fixed-size array while `MyLinkedList` is implemented using a linked list. Since the implementation is different, the code to add and remove items for these two classes will be different. Therefore, the `add(item)` method in the `MyAbstractList` would call a method `add_at(index, item)` which is implemented in the child class `MyArrayList` and `MyLinkedList`. This means that both `MyArrayList` and `MyLinkedList` have `add_at(index, item)` method in their class definitions. However, the implementation of this method is different between the two classes. - -Similarly, the `remove(item)` method would call a method `remove_at(index)` which is implemented in both the `MyArrayList` and `MyLinkedList` classes. The `__getitem__(index)` method is called either when you use the square bracket operator as in `mylist[index]` or the get method as in `mylist.get(index)`. Since the way to access the element is different between the fixed-size array and the linked list, this method should be overridden in the child classes. Similarly, the method `__setitem__(index, value)` would have different implementation in the children classes. Therefore, our implemention of this method would call the method `set_at(index, item)` of the child class' method. - -To summarize, we have implemented two kind of list classes. One is implemented using a fixed-size array while the other one is implemented using a linked list. Both classes inherit from a common base class called `MyAbstractList` which provides the common attributes, properties and methods that all list classes have. In this way, we do not duplicate the codes that are the same in `MyArrayList` and `MyLinkedList`. This common methods and codes are placed in the parent class `MyAbstractList`. Only the different implementation is defined in the child classes' methods. The class `MyAbstractList` inherits and implements `Iterator` class. This ensures that all our list are iterable. To implement `Iterator` class, we must define `__iter__()` method in our `MyAbstractList` which returns an iterator object. Both `MyArrayList` and `MyLinkedList` inherit this iterator method when they inherit from `MyAbstractList`. \ No newline at end of file diff --git a/_Notes/Basic_Data_Types.md b/_Notes/Basic_Data_Types.md new file mode 100644 index 0000000..cd6659e --- /dev/null +++ b/_Notes/Basic_Data_Types.md @@ -0,0 +1,504 @@ +--- +title: Basic Data Types +permalink: /notes/basic-data-types +key: notes-basic-data-types +layout: article +nav_key: Notes +sidebar: + nav: Notes +license: false +aside: + toc: true +show_edit_on_github: false +show_date: false +--- + +## What Kind of Data is This? + +It is important to ask the question, "what kind of data is this?". One of the challenges of novice programmers is that Python does not require you to declare the *data type*. So many novice programmers do not think on this question, "what kind of data is this?". However, it is essential on every part of the problem solving steps, to continue asking the question, what kind of data we are dealing with. + +In the first step `P` for Problem statement, we are asking what is the input, output and the process. In answering these questions, we have to ask what *kind* of data is the input and what *kind* of data is the output. The kinds of data is what we call as *data type*. + +In our first problem, we want to display `Hello World!` into the screen. We should ask, what kind of data is this? + +## Two Basic Data Types + +Now, we will introduce two basic data types: +- text +- numbers + +In our first problem, our data is not numbers but rather text. We call this data of a `string` data type. In some other programming language, they have a character data type where it only consists of a single character. Python does not have a character data type. To create a character, you basically create a string with only one character. + +If you recall about our chatbot, a name will be a string data. But let's say if you create a chatbot to calculate your cadence related to your fitness activity, some of those data may not be a string. For example, the chatbot can ask on the number of steps and the duration of your walk or your cycling period. In calculating your cadence, the app has to work with numbers to do the mathematical manipulation. + +This `string` data type includes other kinds that may not so obviously a text such as: +- new line character +- tab and other whitespaces + +Python supports the standard [ASCII characters](https://www.asciitable.com) and [Unicode](https://home.unicode.org) characters. + +Even what may seem like numbers can be actually a `string` data type. For example, `1234` can be considered as a string instead of numbers. One example would be a student id data. Since this data is more like a label rather than being used with any mathematical manipulation, it is more likely that such data is represented as a string instead of as a number. + +On the other hand, Python supports two kinds of numbers: +- `int`: whole number +- `float`: decimal numbers represented as floating point + +An example for `int` data type would be the whole range of whole numbers: 3, 17, 1234, etc. On the other hand, `float` is used to represent numbers with decimal point. In computer, decimal numbers cannot be represented accurately all the time. The reason is that there are some numbers with infinite decimal expansion. For example, +- 1/11 = 0.090909... +- 10/3 = 3.333333... +- 7/4 = 1.74999... +- etc + +Since computer stores information in a finite space (or finite *bits*), it is not possible for computer to represent these numbers accurately. Therefore, real numbers are stored in computer using a representation called *floating point numbers* that has some finite precision. When dealing with real numbers in computers, we have to accept that we can never eliminiate floating-point errors. We can only [manage floating-point errors by mitigating them](https://en.wikipedia.org/wiki/Floating-point_error_mitigation). + +## Creating a String Data + +Now, we can answer that `Hello World!` is of `string` data type. How can we create `string` data type and use it in our computer programs? There are a few ways of creating a `string` literal in Python. + +The first way is to use *single* double quotes as shown in the previous code. + +```python +"Hello World!" +``` + +You can create this data in the Python shell. When you press enter, it will just display the data you just created. + +```python +>>> "Hello World!" +'Hello World!' +>>> +``` + +Throughout this book, we will use `>>>` to indicate codes that are entered through a Python shell. Notice also in the above that once you press the ENTER key of our keyboard, the shell returns back the value created. You may also notice that the output string printed by the shell has single quotes instead of double quotes. This brings us to the second way of creating a string. + +The second way is to use a *single* single quotes. + +```python +'Hello World!' +``` + +The third way is to use a *tripple* double quotes. +```python +"""Hello World!""" +``` + +And the last one is to use a *tripple* single quotes. +```python +'''Hello World!''' +``` + +Notice that you have to use the same opening and closing quotes to create the string. For example, you will see the following error if you try to use different quotes to create a string literal. + +```python +>>> "Hello World!' + File "", line 1 + "Hello World!' + ^ +SyntaxError: EOL while scanning string literal +>>> +``` + +Remember to read the error from the bottom. In the above code, we use a double quote in the beginning but use a single quote at the end. Python produces the same error. The reason is that Python interpreter cannot find the closing double quotes. + +Why does Python allow multiple ways to create strings? One reason is that single quotes and double quotes can be part of a `string` data. For example, you can create the following data. + +```python +"Hello World! What's up" +``` +Notice that we have a single quote in the text. We can also include a double quote in our text. + +```python +'Yoda spoke, "World, Hello"' +``` + +What is important is that the opening and closing quotes must match. We cannot however, insert a single quote in between like the following. + +```python +>>> 'Yoda spoke, 'World, Hello'' + File "", line 1 + 'Yoda spoke, 'World, Hello'' + ^ +SyntaxError: invalid syntax +``` + +The reason that the syntax is invalid is because Python finds a closing quotes for the string literal just before `World, Hello'`. Python interpret the `string` data to be: `'Yoda spoke, '`. Python does not understand how to interpret the subsequent characters in `World, Hello'` because there is no operator in between the two tokens of a string `'Yoda spoke, '` and a symbol `World`. Notice that Python does not interpret `World` as a `string` data because it does not have an opening quotes. Therefore, Python try to interpret `World` as a symbol or names instead. + +But how about the triple quotes? What is it used for? Python allows you to create multi-line string data using the triple quotes strings. For example, + +```python +"""Hello, +World!""" +``` + +If you type this into the shell, you can see that it records the new line between the comma and the text `World`. + +```python +>>> """Hello, +... World!""" +'Hello,\nWorld!' +``` + +The new line is recorded as `\n` character. The backslash is normally used for an escape character in many programming language. + +## Creating Int and Float Data + +So we now know that we can create `string` data using the various ways discussed in the previous section. How about numbers? How can we create `int` and `float` data in our code? + +To create an `int` data, we just type the literal as it is. + +```python +>>> 4321 +4321 +``` + +When the numbers are long, you can use underscore to make it easier for human to read. For example, this how you can create the following number $4,321,567,000 in Python. + +```python +>>> 4_321_567_000 +4321567000 +``` + +On the other hand, to create a `float` data, we must make use of the `.` character to insert the decimal point. For example, to create 4321.0 data, we do either the following. + +```python +>>> 4321.0 +4321.0 +``` +or +```python +>>> 4321. +4321.0 +``` + +The `0` is not required but the `.` is required to create a `float`. + +## Saying Hello to the Multiverse + +Let's say we have a few worlds which we want to say hello. We can label each world with a number like: +- World no 1 +- World no 2 +- World no 3 +- ... + +We can then say hello to each of the world. + +```python +>>> print("Hello World no. 1") +Hello World no. 1 +>>> print("Hello World no. 2") +Hello World no. 2 +>>> print("Hello World no.3") +Hello World no.3 +``` + +Another way of doing the same thing is by typing the following. + +```python +>>> print("Hello World no. ", 1) +Hello World no. 1 +>>> print("Hello World no. ", 2) +Hello World no. 2 +>>> print("Hello World no. ", 3) +Hello World no. 3 +``` + +What is the different between the two codes above? In the first code, we print the world's label 1, 2 and 3 as part of a single `string` data. On the other hand, in the second code, we print two **kinds** of data. The first data `"Hello World no. "` is a `string` data type. The second of data, however, is not. The data that is displayed into the screen, i.e. `1` is an `int` data type. The comma (`,`) in between the two data is just to separate the different objects to be displayed. + +Why this matters, you may ask. Thinking abou the data matters because we work with different data differently. For example, you can add numbers and increase it sequentially. Later, you will be able to write *loops* to do the above. Imagine if you have a million of worlds. You are not going to write `print()` statements a million times. Instead, you can just do the following. + +```python +for world in range(1_000_000): + print("Hello World no. ", world) +``` + +If the above code looks foreign to you, do not worry. We will discuss about the `for-in` statement, the `range()` function and the role of variables in subsequent lessons. But I hope you can at least identify there are two data types in the above code. The first one is the `int` data which we create using `1_000_000` to represent the number of worlds we have (assuming we only have 1 million worlds). The second one is the `string` data which we create using the double quotes in `"Hello World no. "`. Thinking about "what kind of data is this?" is very important. Knowing what data that is helps us to know what we can do about it. Different data has different operations and ways to manipulate. + +## Running Python Code as a Script + +We have been running our code in a Python shell. We enter the shell by first typing the following in a terminal. +```sh +$ python +``` + +In the above, the symbol `$` represent your terminal's prompt. It may look different depending on what kind of terminal do you use. But throughout this book, I will use `$` symbol to represent a terminal prompt. This means that the one that you have to type in the terminal is simply `python`. + +The second way of running our Python's code is as a *Script*. To do this, create a new *text file* called `01_hello.py`. You may want to use code editor like Visual Studio Code, Atom, Sublime Text, etc, to do this. You can write your code and paste it into your new text file. + +```python +print("Hello World no. ", 1) +print("Hello World no. ", 2) +print("Hello World no. ", 3) +``` + +After you save your text file, you can run this code by running the following command in your terminal or command prompt. + +```sh +$ python 01_hello.py +``` + +You will see an output that looks like this. + +```sh +Hello World no. 1 +Hello World no. 2 +Hello World no. 3 +``` + +The above command assumes you are in the same location as where you save your `01_hello.py`. For example, if you save your `01_hello.py` in your `/Users/my_user/Downloads`, then you need to go into that folder first. The terminal command to go to a particular location is `cd` which means change directory. + +```sh +$ cd ~/Downloads +``` + +In Unix-based OS, the character `~` is used to indicate the home user's directory, which is the same as `/Users/my_user/`. + +If you use Windows operating system, you may store it in something like `C:\Users\my_user\Downloads`. Similarly, you can open your command prompt and type the following + +```dos +> cd $HOME\Downloads +``` + +For Windows' prompt, I normally use `>` symbol instead of `$`. In Windows, `$HOME` stores the location to the user's home directory. + + +## Sequence Matters + +In programming, sequence matters. In fact, this is the first pattern that you will continue to see recurring again in all computer codes. Computer codes in general execute the instruction in sequence **from top to bottom**. When you run your code as a script, the sequence matters because different sequence may create a different output. For example, run the below code which is the same as in the previous one using Python Tutor to visualize the sequence. You can click the "next" button to step through the execution of the code. However, we want you to note the two arrows. The green and the red arrows. The green light arrow indicates the instruction that has *just been executed*. On the other hand, the dark red arrow indicates the instruction that is *about to be executed*. + + + +The output is different from the following one. + + + +The reason that the output is different because the sequence of the instruction is different. In the second one, the hello to world number 3 is executed first before the other two instructions. + +Besides noting that the sequence matters, it is important to understand the concept of **program counter**. The red arrow in the two embedded code above shows the program counter. A program counter **points to the next instruction to be executed**. In most cases, the program counter moves from **top to bottom** in a linear sequence. We will learn ways to alter this behaviour in the subsequent lessons. But for now, it is important to remember that program counter moves in sequence from top to bottom. The program counter indicates which instruction to be executed next. + +## Introducing Variables + +In many cases, we want to **work** with data. This means that our data may change over time. We may also re-use some of the data we have created in other places in our computation and manipulate it. Computers are good to work with numbers but human works better with labels. In most programming language, we are able to create a **variable** that binds a label or a name with a value or data. For example, in the previous problem of saying hello to our multiverse, we can actually store our world's index or number into a variable. For example, + +```python +world_index = 1 +print("Hello World no. ", world_index) +``` + + + +When you click "next" for the first time, Python interpreter will execute the first line `world_index = 1`. The effect of this execution was not apparent as there is no output produced. However, Python Tutor displays that there are three things happening in the **Global frame** which is the memory environment used by Python to execute the code. +- a label or a name called `world_index` was created +- an `int` literal was created with the value `1`. +- the name `world_index` was binded with the literal data `1`. + +When you click the "next" button the second time, Python interpreter executes the `print()` statement and displays the text into the standard output. Note that in displaying this output, Python interpreter encounters a **name** called `world_index`. Python interpreter then tries to find what is the meaning of this name. Python could not find it in any of its built-in keywords and functions and, therefore, check whether `world_index` is **defined** in the *global frame*. When Python interpreter finds the name `world_index` in the global frame and recognize that it is a variable, it evaluates its value, which is `1`. Thus, the above code is evaluated similar to the following code. + +```python +print("Hello World no. ", 1) +``` + +A variable is just a name that is binded to some value. You can change this binding to a different value. + + + +In the above code, when Python interpreter executes line number 3 `world_index = 2`, it does the following: +- Python creates an `int` literal with value `2`. +- Python binds this new literal to the name `world_index`. + +We can see that the value in the *global frame* for `world_index` changes to `2` after executing line 3. + +You may have noticed that we use the equal sign, i.e. `=` in the above code. This **operator** is called the **assignment operator**. It is called the assignment operator because it assigns the literal to a label or a name. + +Note that this assignment is from **right to left**. We cannot swap the sequence. Writing the below code produces an error. + +```python +>>> 1 = world_index + File "", line 1 +SyntaxError: cannot assign to literal +``` + +The error says that you cannot assign something to a literal. Recall that `1` is an `int` type literal and the assignment is from right to left + +The error says that you cannot assign something to a literal. Recall that `1` is an `int` type literal and the assignment is from right to left. With the above code, you are telling python to assign the value associated with the name `world_index` to the literal `1` and Python does not allow this. + +## Checking Data Type + +Python makes it easy for programmers to create variables. But this comes at a cost that programmers do not ask the question, "What kind of data is this?". When you create a variable, binds the data dynamically and does not require you to specify the type of the data. However, Python 3.6 onwards allows programmers to specify the data type as a kind of **annotation** to be checked by other programs. We will use the type annotation supported by Python 3.11. You can specify the previous variable as follows. + +```python +world_index: int = 1 +``` + +However, what Python does is just to create an annotation and does not enforce it. For example, you can still make mistakes assigning a wrong data type and Python interpreter will still executes fine. Run the following code step by step observing the data created in the memory. + + + +As you can see that `world_index` is neither `str` nor `float` and yet Python continues to executes the code. The annotation is useful if you use other checker to check your code. The most common one is called [mypy](https://github.com/python/mypy). + +To install mypy, do the following from the terminal. + +```sh +$ python -m pip install -U mypy +``` + +You can then run `mypy` on your script. To create your script. Create a text file called `02_wrong_type_hello.py` and type in the following code. + +```python +world_index:int = 1 +print("Hello World no. ", world_index) +world_index:str = 2 +print("Hello World no. ", world_index) +world_index:float = 3 +print("Hello World no. ", world_index) +``` + +Running `mypy` on the text file results in the following. + +```sh +$ mypy 02_wrong_type_hello.py +02_wrong_type_hello.py:3: error: Incompatible types in assignment (expression has type "str", variable has type "int") [assignment] +02_wrong_type_hello.py:5: error: Incompatible types in assignment (expression has type "float", variable has type "int") [assignment] +Found 2 errors in 1 file (checked 1 source file) +``` + +To fix this error, you need change the data back to `int` type. Create a new text file with the corrected code with the name `03_correct_type_hello.py`. + +```python +world_index:int = 1 +print("Hello World no. ", world_index) +world_index = 2 +print("Hello World no. ", world_index) +world_index = 3 +print("Hello World no. ", world_index) +``` + +And run `mypy` again. + +```sh +$ mypy 03_correct_type_hello.py +Success: no issues found in 1 source file +``` + +Notice that `mypy` does not actually runs the code but only checks if there is any issues by doing static type checking on the code. We will use `mypy` to improve our code. + +As a programmer, you can actually check what is the data type of a variable using the `type()` function. + +```python +world_index = 1 +print(type(world_index)) +world_index = "2" +print(type(world_index)) +world_index = 3.0 +print(type(world_index)) +``` + + + +Try clicking the "next" button and see how the code works. I think it is important to explain the following line. + +```python +print(type(world_index)) +``` + +When Python interpreter sees this, +- it sees a name `print` and recognize that you are *invoking* a function due to the opening and closing parenthesis `print()`. +- Python tries to evaluates the value inside the parenthesis to pass it to the `print()` function. +- When Python evaluates the value inside the parenthesis, it finds another name `type` and recognize that it is another of its built-in function. +- Python also sees that you are invoking `type()` because of the opening and closing parenthesis. In order to execute this function, Python tries to evaluate the value inside this *inner* parenthesis. +- When evaluating the inner parenthesis, Python finds another name `world_index`. This time, Python cannot recognize this name from any of its built-in keywords or function and tries to find it in the global frame. +- Python finds the name `world_index` in the global frame and obtains the value, which is `1`. +- Python pass this value `1` to the function `type()` and executes `type()`. This functions evaluates to a data that describes Python's `type`. You can actually try to type the following in a Python shell to see: `type(type(1))`. You will get ``. +- Python then pass this output of `type()` function to `print()` function. In order to print it, Python will convert this data into a string so that it can displays it into the standard output. +- Python finally displays `` into the screen. + +Now, that's a lot of things going on with just a simple code. But it is important to note that Python **evaluates from inside to outside of the parenthesis** when it invokes a function such as `print()` and `type()`. We will learn more about function in the next lesson. + +## Environment Diagram + +Python Tutor allows you to see the memory environment of your code. On the right hand side of your code you see two panels, the first one at the top right is the print of your standard output. This panel shows you whatever that you display to the standard output through the `print()` function. The second one is on the right side just below the print output panel. This shows you what is happening in the memory environment of your code. At the beginning, you only see the labels: +- Frames +- Objects + + + +Try clicking the "next" button and you will see that the first frame created is called the **global frame**. Later on, when you learn to create your own user defined function, you will see that each function has its own local frame. But for now, we will only deal with the global frame. All the variables are created in this global frame. Currently, we only have one variable called `world_index`. As we introduce more data and more different kinds of data, you will see more things in this environment diagram. + +## Identifying Data Type in a Problem + +The first step of PCDIT is Problem Statement. In this step, we need to identify the input, output and process of the problem. In identifying the input and the output, we need to ask, "what kind of data is this?". + +Let's try to identify the input and output of our chatbot. Let's say, our chatbot is able to advise us on our fitness activity and would like to calculate your cadence from your previous cycling period. So the chatbot will do a conversation like the following. + +``` +Hi, Jane! How as your last cycling? + +> nice + +Did you count how many push you did on the pedal within 30 seconds? + +> 25 + +Thanks. Your cadence is 50. That's rather low, you may want to try to increase it. +``` + +Now, we should ask, what the input and the output of this program is. The input to the chatbot is all the data that you give to the program. In this case, they are text data as you type into the chatbot. This means that the data is a string data. On the other hand, the chatbot displays a string data as well into the app screen. So both input and output are string. + +You may ask why the input is not a number since `25` looks like a number. The answer is that the data that you key in is considered as a string because it is entered into a kind of text field. This is why it is important to ask what kind of data it is. + +However, in order to calculate the cadence, you cannot manipulate a string data and therefore you need to transform your data into a number-like data. Since steps tend to be generally whole number, we can convert that string data into an `int`. So now we can roughly design our algorithm to calculate your cycling cadence. +1. Request for number of steps within 30 seconds +1. Convert the number of steps from string into integer +1. Multiply number of steps by 2 to get the cadence + +Depending on the cadence we may want to decide whether to prompt the user to increase their cycling cadence or not. But in order to display different messages, we will need to learn on the control structure in the subsequent lesson. For now, we can implement the cadence calculation. + +In order to get data, you need to learn a function to take in data from the keyboard. In Python, you can use `input()` function where the argument is the prompt you want to display to the user. This function returns you the string that the user enters through the keyboard. + + +```python +steps_inp: str = input("how many push did you do on the pedal within 30 seconds? ") + +steps: int = int(steps_inp) +cadence: int = steps * 2 + +print("Your cadence is ", cadence) +``` + + + + +Try clicking the "next" button step by step. When Python Tutor loads, you will see a prompt to enter the number of steps. In the embedded Python Tutor, a value of 25 was already entered. If you want to see the prompt and enter it yourself, you can click [this link](https://pythontutor.com/visualize.html#code=steps_inp%3A%20str%20%3D%20input%28%22how%20many%20push%20you%20did%20on%20the%20pedal%20within%2030%20seconds%3F%20%22%29%0A%0Asteps%3A%20int%20%3D%20int%28steps_inp%29%0Acadence%3A%20int%20%3D%20steps%20*%202%0A%0Aprint%28%22Your%20cadence%20is%20%22,%20cadence%29&cumulative=false&curInstr=0&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=3&rawInputLstJSON=%5B%5D&textReferences=false). Now you see the variable `steps_inp` was created in the global frame with a value `"25"`. You can note that the data type is a string. + +The next instruction is to convert the data into an `int` using the `int()` function. Notice the common way of calling the function using the parenthesis. The output is stored in a new variable called `steps`. After you click "next" you see that the value of `steps` and `steps_inp` are different. One is an `int` while the other is a `string`. + +In the next instruction, we calculate the `cadence` by multiplying the number of push within 30 seconds. And lastly, this number is printed into the standard output. + +You can try running the above Python code as a script. To do that, create a new file, say with the name `04_cadence_bot.py`. Paste the code into this text file. You can run the code then by typing the following into the terminal. + +```sh +$ python 04_cadence_bot.py +``` + +The output and interaction may look like something of the following. + +```sh +$ python 04_cadence_bot.py +how many push you did on the pedal within 30 seconds? 23 +Your cadence is 46 +``` + +In the above code, the output cadence is also an `int` but then we display this value into the standard output. In later lessons, we will learn on Python's string formatting so that we can format any data as a string to display them in the standard output in a better way. + +## Summary + +In this two lessons, you have learnt how to write your first code. The first important pattern that we introduce here is what we call as **sequential**. In general, Python code is executed in sequence from the top to the bottom. We will learn how to change this sequence to allow variation of code to be executed depending on different conditions. + +We also learn our first built-in function, which is `print()`. We learn how to call a function using the parenthesis and supplying the input data to the `print()` function through its argument. We will create our own custom function in the next lesson. We also made use of `int()` function to convert a string data into an `int` data type. + +How would we know that we need to use `int()` function in our last example of cadence chat bot? The answer is because we were asking the question, "What kind of data is this?". In our use of `input()` function (the other built-in function that we use), the output of this function is a `string` data. Because it is a string data it cannot be used with any other mathematical operators to calculate any new data. But an `int` data is a numeric data that can be manipulated using mathematical operators. So we convert it to an `int` first before we calculate the cadence. Asking what kind of data that we are dealing with is a necessary components in PCDIT framework. So in this P (problem statement) step, we also ask that question. That helps us to know that we need a step to convert the data from one type to another type. + +We have introduced a few basic data types, `string`, `int` and `float`. These are the basic data and it allows us to do a simple chatbot example to calculate cadence when you cycle. In subsequent lessons, we will introduce more data and when you learn about object oriented, you will be able to create your own custom data. + +In the next lesson, we will dive in into **functions**, which is our first lesson on abstraction. Remember that abstraction is one of the big element in computational thinking. You will see abstraction every where in learning how to code and creating a function is one of the first example of abstraction that we will introduce. + + diff --git a/_Notes/Basic_Operators.md b/_Notes/Basic_Operators.md new file mode 100644 index 0000000..d1f3e4b --- /dev/null +++ b/_Notes/Basic_Operators.md @@ -0,0 +1,500 @@ +--- +title: Basic Operators +permalink: /notes/basic-operators +key: notes-basic-operators +layout: article +nav_key: Notes +sidebar: + nav: Notes +license: false +aside: + toc: true +show_edit_on_github: false +show_date: false +--- + +## Operators and Operands in Computation + +In our previous lessons, we have done some simple computations. We first computed the cadence and then the speed based on the cadence and the bicycle parameters. In all these we use mathematical **operators** to do our computation. For example, to calculate the cadence from the number of steps, we had the following function definition. + +```python +def compute_cadence_for_30sec(steps): + return steps * 2 +``` + +In the above function definition, we used the **multiplication** operators, i.e. `*`. Multiplication operator is an example of what we call as **binary operator**. The word binary refers to the number of the operands which in this case is two for the multiplication operator. This means that multiplication operator requires *two operands*, that is the left hand side number and the right hand side number which the operator will multiply. Python expects two operands and when there is a lack of operand, Python will throw an exception or an error as in the code below. + +```python +>>> cadence = 2 * + File "", line 1 + cadence = 2 * + ^ +SyntaxError: invalid syntax +``` + +In the above code, the multiplication operator only has the left operand and it gives a `SyntaxError`. + +We can think of operator as another primitive abstraction concept of a computation. Recall that every computation may take in output and produce output. In this case, an operator takes in the operands as its input. Moreover, the operator is evaluated and produces an output value. Python evaluates the expression and produces a value. For example, in the expression `steps * 2`, Python first evaluates the variable reference `steps` and get its value. This value and a literal integer of `2` are the operands. Python then evaluates the multiplication using these two operands as the input to the multiplication function. In this way, operators are convenient way of expressing a computation as compared to calling a function such as the following. + +```python +cadence = multiply(steps, 2) +``` + +Using the `*` multiplication operator is more expressive and readable for human. At the same time, you can still conceptually think operator as a primitive computational unit that takes in input and produces output. + + + +## Math and Assignment Operators + +Python supports the common arithmetic operators as shown in the table below. We added the assignment operator together here in the last row to show that all the math operators will be evaluated first before it is being assigned. + +| precedence | operator | remarks | +|------------|----------|--------------------------------------------------------------------------------------------------------------------------------| +| 0 | () | Parenthesis is used to evaluate the inside expression to override the precedence of the surrounding operators. | +| 1 | ** | Power or exponentiation operator. | +| 2 | * / % // | Multiplication, division, modulus and integer division. They are evaluated from left to right when appearing in a single line. | +| 3 | + - | Addition and subtraction. | +| 4 | = | Assignment operator. | + +You may be familiar with the common arithmetic operators such as: +- `+` for addition +- `-` for subtraction + +These two are at the lowest precedence which means that they are evaluated last. Just as in normal arithmetic, the multiplication and division has higher precedence as compared to addition and subtraction. +- `*` for multiplication +- `/` for division + +Starting from Python 3, Python uses `/` operator for *true division*. This means that division of two integer numbers results in a `float` data type. + +```python +>>> 3 / 2 +1.5 +>>> 4 / 2 +2.0 +>>> +``` + +Notice that even when `4` is divided by `2` which may output an int, Python `/` operator still outputs a float. If you want to have an *integer division*, you can use the `//` operator. +- `//` for integer division and to get a quotion of from a division +- `%` for modulus to get a remainder from a division + +```python +>>> 3 // 2 +1 +>>> 3 % 2 +1 +>>> 4 // 2 +2 +>>> 4 % 2 +0 +``` + +Multiplication, division, integer division and modulus have the same precedence. When they appear, Python will evaluate them from left to right. + +Python also supports: +- `**` for power or exponentiation + +```python +>>> 2 ** 3 +8 +``` + +The above computation gives the output of $2^3$. In the case of multiple exponentiation, they are evaluated from right to left. + +```python +>>> 2 ** 2 ** 3 +256 +>>> 2 ** (2 ** 3) +256 +>>> (2 ** 2) ** 3 +64 +``` + +As the three codes above showed, when we have a multiple of `**` operators, it will be evaluated from right to left. In this case, $2^{2^3}$ is evaluated as $2^8 = 256$ instead of $4^3 = 64$ + +Notice that we actually have used the parenthesis `()` to enforce our precedence. This can be used also for example, if we want to do the lower precedence operators such as addition and subtraction before multiplication or division. For example, to evaluate $(2+3)*4$, we use the parenthesis to evaluate the addition first. + +```python +>>> (2 + 3) * 4 +20 +``` + +In the table above, we added the assignment operator `=` at the bottom of the table to show that all the previous operators are evaluated first and only at the last that the assignment operator will be executed. + +```python +>>> x = (2 + 3) * 4 +``` + +In the above code, first the parenthesis will enforce the addition operator to be executed first. Second, the multiplication operator will be evaluated. On then, the assignment operator will be executed. + +Assignment operator is also a binary operator but it does different thing to the left and the right hand side of the operator. Python evaluates the *right hand side* of the assignment operators and bind the value to the name in the *left hand side* of the assignment operator. This direction cannot be interchanged. The followign code gives an error in Python. + +```python +>>> (2 + 3) * 4 = x + File "", line 1 +SyntaxError: cannot assign to operator +``` + +Python only allows assignment to *names*. + +## Compound Operators + +Python also supports some compound operators. The reason is that certain operators are pretty common and it is convenient to write a shorter expression of it. For example, in an iterative structure, we commonly have a counter that we increase. The following computation is common in iterative structure. + +```python +x = x + 1 +``` + +The above expression simply adds the value of `x` by `1` and assign it back to the variable `x`. Remember that the right hand side of the assignment operator will be evaluated first, which in this case is `x + 1`. The assignment operator assign the value produced in the right hand side to the name in the left hand side. + +Since the above expression is so common, Python provides a shorter version of it. + +```python +x += 1 +``` + +You can try the following code. + +```python +>>> x = 0 +>>> x = x + 1 +>>> x += 2 +>>> x +3 +``` +In the above code, the value of `x` was 0 initially. It was then increment by `1` using the `+` operator and increment another time by `2` using the compound `+=` operator. At the end, the value of `x` is `3`. + +Python has several other compound operators for the various math operators: +- `x -= 3` is the same as `x = x - 3` +- `x *= 3` is the same as `x = x * 3` +- `x /= 3` is the same as `x = x / 3` +- `x //= 3` is the same as `x = x // 3` +- `x %= 3` is the same as `x = x % 3` +- `x **= 3` is the same as `x = x ** 3` + +In all these compound operators, the right hand side can be any other Python's expressions. For example, you can have something like the following. + +```python +x += calculate_speed(25) +``` + +In the above code, the right hand side is a function call to `calculate_speed()`. + + +## String Operators + +We have discussed how we can use some of the common arithmetic operators to do mathematical computations. These are done mainly on numeric data such as `int` and `float`. The other basic data type is `string`. String data type have their own unique operations and yet they may use a similar symbol for its operators. Let's look at some of them. + +In arithmetic, a `+` operator is used for addition of two numbers. In string data, however, it is used to **concatenate** or join two strings. + +```python +>>> str1 = "Hello" +>>> str2 = "World" +>>> combined = str1 + str2 +>>> print(combined) +HelloWorld +``` + +Notice that the same operator `+` behaves differently depending on the data type. This is one of the main reason is important for us to ask, "What kind of data is this?". What happens if we try to add two different kinds of data like a string and a number? + +```python +>>> "Hello" + 2 +Traceback (most recent call last): + File "", line 1, in +TypeError: can only concatenate str (not "int") to str +``` + +The error says that we can only concatenate string to string and not to an integer. It is important, therefore, to know what kind of data we are working on to ensure that we do the right computation. + +Some string operators actually can work with numeric data. An example of this is the `*` operator. In arithmetic, this operator is used for multiplication. In a string data, however, it is used to duplicate and join the string together. + +```python +>>> "Hello" * 3 +'HelloHelloHello' +``` + +This operator `*` in fact requires one of the operand to be an integer. It won't work if we use either `float` or another `str`. + +```python +>>> "Hello" * 3.5 +Traceback (most recent call last): + File "", line 1, in +TypeError: can't multiply sequence by non-int of type 'float' +>>> "Hello" * "a" +Traceback (most recent call last): + File "", line 1, in +TypeError: can't multiply sequence by non-int of type 'str +``` + +Some string operations are totally different from arithmetic comptutations. We will go through some string operations in future lessons. + +## Testing and Debugging using Print Statement + +As we introduce more things into our code, it is best to start the habit of **debugging** our code. No programmers can ever written a code without debugging it (Okay, maybe we can write a print Hello World code without debugging it). Most code as it grows in complexity requires programmers to test its code and check whether the code runs as what it is expected it to do. Instead of testing the code at the end of writing a long lines of code, programmers do test their code in bite-size. + +In this section, we want to introduce a little habit which should be part and puzzle of every novice programmers when writing code. This habit is to test their code after one or few lines of code to see whether it performs what it is expected to do. One simple way is to use the `print()` function to test two things: +- the expected value of computation +- the data type of some value + +Let's revisit our `calculate_speed()` function. We will put here the final version, but we will describe how a novice programmers can write it step by step with the print statement to test it. + + +```python +import math +def calculate_speed(cadence: int, + diameter: float = 685.8, + tire_size: float = 38.1, + chainring: int = 50, + cog: int = 14) -> float: + ''' + Calculates the speed from the given bike parameters and cadence. + + Parameters: + cadence (int): Cadence in rpm. + diameter (float): Diameter of the wheel in mm. Default value = 685.8 mm. + tire_size (float): Size of tire in mm. Default value = 38.1 mm. + chainring (int): Teeth of the front gears. Default value = 50. + cog (int): Teeth of the rear gears. Default value = 14. + + Returns: + speed_kmh (float): cycling speed in km/h + ''' + gear_ratio: float = chainring / cog + speed: float = math.pi * (diameter + (2 * tire_size)) \ + * gear_ratio * cadence + speed_kmh: float = speed * 60 / 1_000_000 + return speed_kmh +``` + +So how do we start writing the above code and test it step by step. The *first step* is to write the functin header and immediately create a function call with some actual arguments. In our PCDIT framework, the **C**oncrete Cases can provide some of the actual arguments to be used. In our example, we can start with the following code. + +```python +def calculate_speed(cadence: int, + diameter: float = 685.8, + tire_size: float = 38.1, + chainring: int = 50, + cog: int = 14) -> float: + pass + +print(calculate_speed(25)) +# print(calculate_speed(25, 600, 30, chainring=60, cog=16)) +# print(calculate_speed(25, diameter=600, tire_size=30, chainring=60, cog=16)) +# print(calculate_speed(25, diameter=600)) +``` + +We have written down our function header starting from the `def` keyword using the information that we have from our **P**roblem Statement step. In this step, we know what are our input to our computations and we know the output. We also ask what kind of data they are, both for the input and the output. + +Outside of the function definition, we have four (4) function calls to test different way the function may be called. We can write more, but for simplicity we only list four of them here. + +```python +print(calculate_speed(25)) +``` + +This function call test if we supply the required input argument for `cadence` and use the default values for the rest of the input arguments. + +```python +# print(calculate_speed(25, 600, 30, chainring=60, cog=16)) +# print(calculate_speed(25, diameter=600, tire_size=30, chainring=60, cog=16)) +# print(calculate_speed(25, diameter=600)) +``` + +In these few other function calls, we put comments for subsequent tests. But we will start with that first one call first to test. We have also put Python keywords `pass` as the body of the function. This keyword does not do anything but it is useful when we want to leave the body of the function empty. Without putting this keyword, the functin will give an error. The error is caused because Python expect some indented code after the function header. In fact, after every statement that ends with the colon `:`, it expects some indented block. + +We can save the above code into a file `01_calculate_speed_1.py` and run both `mypy` and `python`. + +```sh +$ mypy 01_calculate_speed_1.py +01_calculate_speed_1.py:1: error: Missing return statement [empty-body] +Found 1 error in 1 file (checked 1 source file) +``` + +In this first run of `mypy`, it detects that our function has a `float` output from the functin header but we do not have any `return` statement. So it throws an error. This is a good reminder. Let's fix our code to return something and run `mypy` again. + +```python +def calculate_speed(cadence: int, + diameter: float = 685.8, + tire_size: float = 38.1, + chainring: int = 50, + cog: int = 14) -> float: + return speed + +print(calculate_speed(25)) +``` + +In the above code, we have removed the other function calls for simplicity but we added the `return speed`. The output of running `mypy` on the above code is shown below. + +```sh +$ mypy 02_calculate_speed_2.py +02_calculate_speed_2.py:6: error: Name "speed" is not defined [name-defined] +Found 1 error in 1 file (checked 1 source file) +``` + +Now, `mypy` complains that `speed` has not been defined. Python simply doesn't know what `speed` is. To fix this, we can tell Python that `speed` is our output variable, but since, we have not done any computation yet, we can assign it to `0.0` object for now. + +```sh +$ mypy 03_calculate_speed_3.py +Success: no issues found in 1 source file +``` + +Now `mypy` has no complain. Notice that we assign `speed` to `0.0` instead of `0`. You can try assigning it to `0` but `mypy` will complain that the output is expected to be a `float` instead of an `int`. Anyway, we will compute `speed` and hopefully it will be `float` data at the end. + +So now, we can run `python` to execute the code. Python will register the function definition and do a function call. However, it will currently output `0.0`. + + + +Now, we can start writing our code. One of the useful thing to do is actually to check that you have the input you expected it. So we can start using our `print()` function to test this. + + + +In the code above, we print both the value of `cadence` and its type. If you annotate your input arguments, this may not be necessary as `mypy` would have checked that for you. However, type annotation is optional in Python and we just want to show you that it is useful to check the type of your input as well. You can also print all the other values of the input arguments such as `diameter`, `tire_size`, etc. + +Now, we can calculate the gear ratio from the given `chainring` and `cog`. Let's add the code and print the values. + + + +We printed both the value and the type. Since we expected the value of the gear ratio to be a float, we added a type annotation for `gear_ratio` as `float`. The output is given as shown below. + +```sh +3.5714285714285716 +0.0 +``` +The first line comes from the print statement for the `gear_ratio`. The second line comes from the print statement in line 11 which is the output of the function call `calculate_speed(25)`. Recall that `speed` is still assigned to `0.0`. Notice also that we have removed the print statement for the `speed`. These print statements are used for us to quickly test our code and we can always remove it when it is not neeeded. + +Now, we are ready to calculate the speed. Let's put in the expression using the mathematical operators we have discussed. + +```python +def calculate_speed(cadence: int, + diameter: float = 685.8, + tire_size: float = 38.1, + chainring: int = 50, + cog: int = 14) -> float: + speed = 0.0 + gear_ratio: float = chainring / cog + speed: float = math.pi * (diameter + (2 * tire_size)) \ + * gear_ratio * cadence + print(speed) + return speed + +print(calculate_speed(25)) +``` + +This is what happens when we run `mypy`. + +```sh +$ mypy 04_calculate_speed_4.py +04_calculate_speed_4.py:8: error: Name "speed" already defined on line 6 [no-redef] +04_calculate_speed_4.py:8: error: Name "math" is not defined [name-defined] +Found 2 errors in 1 file (checked 1 source file) +``` + +There are two issues here. +- The first one is that the name `speed` is defined two times. There is nothing wrong in Python to redefine a variable name, but it is best to keep the definition to be the same. Otherwise, it will be hard to debug our code if the value or its type can change over the execution of the code. +- The second error is that `math` is not defined. Python does know what `math` is and so we can simply fix it by importing the `math` library. This happens because we make use of the `math.pi` constant in our equation. + +To fix it, let's remove `speed = 0.0` and import `math` to our code. + +```python +import math +def calculate_speed(cadence: int, + diameter: float = 685.8, + tire_size: float = 38.1, + chainring: int = 50, + cog: int = 14) -> float: + + gear_ratio: float = chainring / cog + speed: float = math.pi * (diameter + (2 * tire_size)) \ + * gear_ratio * cadence + print(speed) + return speed + +print(calculate_speed(25)) +``` + +Now, `mypy` will report no errors. + +```sh +$ mypy 05_calculate_speed_5.py +Success: no issues found in 1 source file +``` + +The output when running `python` is as follows. + +```sh +$ python 05_calculate_speed_5.py +213740.50018173413 +213740.50018173413 +``` + +Now we got two values that are the same because we have two print statements. The first print statement is inside the function definition which is just before the `return` statement. The second print statement is in the function call at line 14. You can also run the code in this Python Tutor. + + + +But this is not the value that we expected. We expected the speed to be about 12.8 km/h. The reason is that the units are wrong. This is why it is useful to print the value value of our computation. Though there are no longer syntactical errors, our code may not produce the correct output. To convert to km/h, we can add the conversion calculation. + +```python +import math +def calculate_speed(cadence: int, + diameter: float = 685.8, + tire_size: float = 38.1, + chainring: int = 50, + cog: int = 14) -> float: + + gear_ratio: float = chainring / cog + speed: float = math.pi * (diameter + (2 * tire_size)) \ + * gear_ratio * cadence + speed_kmh: float = speed * 60 / 1_000_000 + print(speed_kmh) + return speed + +print(calculate_speed(25)) +``` + +In this code, we replaced printing `speed` to printing `speed_kmh` which is our speed in km/h. Running this code using `mypy` produces no errors. + +```sh +$ mypy 06_calculate_speed_6.py +Success: no issues found in 1 source file +``` + +However, it produces two outputs. + +```sh +$ python 06_calculate_speed_6.py +12.824430010904047 +213740.50018173413 +``` + +The reason is that the return value still returning `speed` instead of `speed_kmh`. We can fix this by changing the return variable to `speed_kmh`. + +```python +import math +def calculate_speed(cadence: int, + diameter: float = 685.8, + tire_size: float = 38.1, + chainring: int = 50, + cog: int = 14) -> float: + + gear_ratio: float = chainring / cog + speed: float = math.pi * (diameter + (2 * tire_size)) \ + * gear_ratio * cadence + speed_kmh: float = speed * 60 / 1_000_000 + return speed_kmh +``` + +Now, we get the output that we expected. + +```sh +$ mypy 06_calculate_speed_6.py +Success: no issues found in 1 source file + +$ python 06_calculate_speed_6.py +12.824430010904047 +``` + +You can also run in in Python Tutor below. + + + +Previously, we have commented the other function calls with different values of input arguments. You can remove the comment now and test. You may need to calculate it using your calculator just to make sure that it outputs the right speed using the equation when the input arguments are different. + +We will show how to test for the various input arguments and using the `assert` statement in future lessons. For now, we just want to show that you can use `print()` statement in stages to test your code in bite-size and ensure that it produces what you expected. We have made use of `mypy` as well to do some of the checking on our behalf. \ No newline at end of file diff --git a/_Notes/Basic_Structures.md b/_Notes/Basic_Structures.md new file mode 100644 index 0000000..9ca7e1c --- /dev/null +++ b/_Notes/Basic_Structures.md @@ -0,0 +1,456 @@ +--- +title: Basic Control Structures +permalink: /notes/basic-structures +key: notes-basic-structures +layout: article +nav_key: Notes +sidebar: + nav: Notes +license: false +aside: + toc: true +show_edit_on_github: false +show_date: false +--- + +## Basic Control Structures + +We will introduce some basic control structures in this section. You will find these structures throughout many computer codes and programming exercises. In fact, we can actually reduce the control structure of all programming code into only a few control structure. The image below show the three basic control structure. + + + +The first one on the left is **sequential** and we have been using this structure throughout our previous codes. We claim that computer codes are executed sequentially from top to bottom. This is the most basic and fundamental structure of all computer codes. + +In this lesson and the next lesson, we are going to introduce the second structure which is a **branch** structure as shown in the middle of the above image. A branch structure adds an extremely important features in programming becauses it allows you to execute a certain code instead of another. Branch structure allows you to change the flow of the code based on some conditions. + +Lastly, we will introduce briefly the **iterative** structure as shown on the right in the above image. Iterative structure is based on the branch structure because it uses the branch structure to determine whether to continue or to stop the iteration. In the above image, if the condition is true, the computer will continue to the next iteration. On the other hand, if the condition is false, the computer will stop the iteration and execute the next block of the code. + +Notice that both branch and iterative structure relies on sequential structure for each of the block that they will execute. Sequential structure remains the fundamental control flow of a computer code. Branch structure adds on the possibility of changing this sequential flow based on some conditions and Iterative structure allows you to repeat a block of code as long as the condition is true. + +## Sequential Structure + +An example of sequential structure is what you have seen previously. Let's put the code and draw a **flow chart** of the code. + +```python +import math +def calculate_speed(cadence: int, + diameter: float = 685.8, + tire_size: float = 38.1, + chainring: int = 50, + cog: int = 14) -> float: + ''' + Calculates the speed from the given bike parameters and cadence. + + Parameters: + cadence (int): Cadence in rpm. + diameter (float): Diameter of the wheel in mm. Default value = 685.8 mm. + tire_size (float): Size of tire in mm. Default value = 38.1 mm. + chainring (int): Teeth of the front gears. Default value = 50. + cog (int): Teeth of the rear gears. Default value = 14. + + Returns: + speed_kmh (float): cycling speed in km/h + ''' + gear_ratio: float = chainring / cog + speed: float = math.pi * (diameter + (2 * tire_size)) \ + * gear_ratio * cadence + speed_kmph: float = speed * 60 / 1_000_000 + return speed_kmph +``` + +We can draw a flow chart of the above code as shown below. + + + +As you can easily see from the flowchart, the computation flows from top to bottom. The sequence is important because we need to compute the `gear_ratio` before we can compute the `speed`. Similarly, we need to compute the `speed` before we can compute the `speed_kmph`. + +In the image, we have put a rounded rectangle as the starting point of the flowchart. We also indicated the input arguments into the start process. On the other hand, all the computation processes use a normal rectangle. The flow ends with the return statement and we indicate the return value in the end terminal symbol which is also a rounded rectangle. + + + + +Let's compare this with another code that you have previously written as well and draw the flow chart as well. + +```python +import math +from typing import Tuple + +Speed = Tuple[float, float] + +def calculate_speed(cadence: int, + diameter: float = 685.8, + tire_size: float = 38.1, + chainring: int = 50, + cog: int = 14) -> Speed: + gear_ratio: float = chainring / cog + speed: float = math.pi * (diameter + (2 * tire_size)) \ + * gear_ratio * cadence + speed_kmph: float = speed * 60 / 1_000_000 + speed_mph: float = speed * 60 / 1.609e6 + return speed_kmph, speed_mph +``` + + + +The above code is similar to the previous one. The only difference is that we compute both `speed_kmph` and `speed_mph` in this code. At the same time, you may have noticed that we don't put a separate process box for the `speed_mph` and `speed_kmph`. The reason is that these two computations does not depend on each other and can be computed as long as `speed` has been computed. This means that the two computations are at the same dependencies with respect to the `speed` process block. + +## Branch Structures + +Let's say, now, we want to train at a consistent rate of cadence and we have a specific target to hit, we can monitor our performance whether we hit the target or not using a branch structure. For example, if our target for cycling cadence is 60 RPM, then we can have the following flow chart. + + + +In the above flowchart we have introduced a diamond symbol which is used to indicate a **decision** process. A Decision process allows a branch to more than one process. Inside that decision, a condition has to be specified. If the condition is true, a certain process will be executed (block A). Otherwise, another process will be executed. In our example here, if the cadence is below 60 RPM, then the user does not hit the target. Otherwise, the user hits the target. + + + +In the above flowchart, we show that we will display "You are below target" if the cadence is less than 60 RPM. On the other hand, we display "You hit your cadence target of 60RPM" if the cadence is 60 or above. + + + +Notice that we use the parallelogram to indicate the process of displaying an output. This symbol is used for both **input** and **output** process. An input process may be something like getting the data from the user through a keyboard. An output process can be something like what we did here which is to display some data into the screen. + +We can modify the branch structure to be more complicated than the above. The above structure only allow the program to flow into either two options depending on the condition. If the condition is true, it will do one thing, otherwise, it will do another thing. What if we have more than two things to choose? For example, let's say if we want to categorize the user's cadence into the following table. + +| cadence | category | +|------------|-----------| +| <70 | low | +| 70 to <90 | moderate | +| 90 to <100 | high | +| 100 to 120 | very high | + +In this case, we can draw the flowchart as shown below. + + + +Notice that in the above flowchart, we choose to start from the very high category and move down. You can see the substructure of the flowchart is basically just a simple branch structure that is nested in a process when the condition is false. + +Can we choose the other way around starting from the low category? + + + + +Notice that if the condition `100 <= cadence <= 120` is true, we display "very high". However, if this condition is false, we do not display anything and there is no process associated to it. Though, we can write a code for this, it is easier to read if we start from the "very high" category and move down such that all the other cases when the cadence is lower than 70 is attributed to "low". This example, illustrates that though we can choose how we want to nest and sequence our conditions, certain way of writing is easier to read. + +## Iterative Structures + +Another common structure that we often find in computing is called **iterative** structure or **loops**. Iterative structure is based on the *branch* structure because it requires the sequence of the code to be altered based on some conditions. The only difference is that for iterative structure one of the branches will loop back to the top to repeat some codes in the body of the loop. + +Let's take a look at an example how iterative structure can appear in our cycling chatbot. For example, let's say we want the user to enter the number of steps to calculate the cadence. Some users may enter non-valid data such as a word instead of a number or other non-relevant information. What would the chatbot do? The chatbot can repeat the question until the user enters a valid data. + +Let's recall how we can get input from the user using Python's `input()` function. + +```python +steps_inp: str = input("how many push did you do on the pedal within 30 seconds? ") +``` + +How can we repeat this as long as the user did not enter a valid response? Below is a flowchart on how you can do so. + + + +A few notes on the flowchart diagram above. Notice that we use the parallelogram for the `input()` function to get the users' input. We do this two times: the first one is the initial prompt and the second one is when the user did not enter a valid input. We modified the prompt to the user to indicate that the input entered is not valid and give some valid input example. + +Every time the user enters the input, it will be directed to the decision diamon box where it will check if the input is valid or not. If it is not valid, it will repeat the blocks that requests the user to enter it again. On the other hand, if the input is valid, it will exit the loop and continue to the next part of the code. + +The iterative structure is very common and can be found in many different cases. Another example could be when we want to average the cadence values for the past three days. We can create three variables to store the three days cadence values. Another way is to store it in a **collection** data type such as a list. We will cover list in future lessons. For now, you can see a list data structure as just a list of data. You can name this list `cadence` for example. If you want to store only the past three days, the list can have three elements. On the other hand if you want to store for the past one week, the list can contain seven elements. List is very flexible and very useful in programming. + +Let's go back to our problem in calculating the average cadence for the past `n` days. Below is the flowchart that we can draw to compute the average. + + + +The key part of the above flowchart is the process to compute the `total`. The `total` is computed by adding the previous total with the value of `cadence` at a particular `day`. We then increase the `day` by one to get the value of the `cadence` of the next `day`. The `average` is just `total` divided by the number of day, i.e. `n`. + +In the above flowchart, notice where the loop structure is. Notice also that we always have some part of the flowchart that is sequential. Recall that we mention that the sequential structure is the most basic structure. In general, any iterative structure *usually* have the following structure. + + + +The init block is used to initialize the data which will be used to decide whether we will enter into the loop or not. This data is checked with some conditions in the diamond decision block. Depending on the condition, we either enter the **body** of the loop or **terminate** the loop. It is possible that we may never enter the body of the loop for some cases. The condition simply determines whether to do the body of the loop or not. The body of the loop consists of two parts, the first part is the main code to be executed repeatedly. The second part is the code to modify the data in such a way that the condition at some points in time will *terminate* the loop. If we do not have this block that modifies the conditions, the loop will run forever and we will end up in an **infinite** loop. The program will hang and will never continues. + +## Identifying Structural Patterns in a Problem + +We will see the three basic structures again and again. It is important for us to be able to identify which structure may be present in a given problem that we are solving. As a basic rule, the sequential structure is present throughout and is the most basic structure. All computation is fundamentally sequential. This means that the sequence matters and we compute from the top to the bottom. Therefore, in this section, we are more interested in identifying if we can spot the *branch* structure and the *iterative* structure in a given problem statement. + +The way we will do this is to introduce the next part of our problem solving framework, which is the **C**oncrete cases and the **D**esign of Algorithm steps. Previously, we have discussed the **P**roblem statement step in identifying the input, output, the computation process that is needed. We also shared that we need to ask the question, "What kind of data is this?" at every step of our PCDIT framework. + +These two steps in PCDIT is best illustrated with an example. Let's start with a problem. Let's say we want to train cycling cadence to hit a certain target average for the past one week. If the user hit the target, the chatbot would like to compliment the user for achieving the target and maybe give some bonus points through its gamification features. IF the user does not hit the target, the chatbot may encourage the user to try harder or maybe to set a lower target. Whether the chatbot requests the user to set a lower target or simply encourage him to try harder will be based on the difference that the user's cadence average with the target. If the difference is greater than 10 RPM, then the chatbot will ask the user to have a lower target. How should we start? Let's apply the PCDIT framework and in the process, we will identify if there is any *branch* structure or *iterative* structure. + +### Problem Statement + +We will start with the problem statement. In this step, we will ask what is the input, output and the computation process. We are also interested in the kind of data of the input and output. + +Let's say for our case, the input is a list of cadence of the user for the past seven days. This input is a collection data type. In such collection data type, we want to ask further what is the data type of the element of the list. In our case, the cadence is of `int` type in RPM (rotation per minute). + +There is another input in our case. This is the target cadence average the user wants to hit. Since this is a cycling training app, the user may want to hit a certain target cadence for the past one week. This target value is an input to this computation. What's the data type? This can be another integer. + +How about the output? There is no particular output that this code will return. The program, however, will end up in several possible states. +- state 1: The user hits the target and the chatbot displays some compliment and reward user with some bonus point. +- state 2: The user does not hit the average target by less than 10 RPM difference. In this case, the chatbot will display some words to encourage the user to hit the target. +- state 3: the user does not hit the average target by more than 10 RPM difference. In this case, the chatbot will offer the user whether he or she wants to modify the target with a lower target at first. + +What we have described in the previous paragraph is part of the computation process that we need to perform. In order to arrive at one of those states, the program has compute the average cadence first. We can summarize the problem statement as follows. + +``` +Input: + - cadence_list: list of cadence for the past seven days. The element of the list is an integer. + - target_cadence: the target value for the cadence average to hit. + +Output: + - None + +Process: + - Compute average cadence from the list + - From the average, set the state of the chatbot one of the three states in the table. +``` + +### Concrete Cases + +In concrete cases steps, we try to think of specific values for the input and work out the computation step by step. The important part is take note and observe how we do the computation. This step leads to the **D**esign of Algorithm step. + +To do this, let's create some concrete cases for the input. For example, we can start with the following input list for the cadence in the past seven days and some target value. + +```python +cadence_list = [45, 57, 62, 58, 55, 66, 63] +target_cadence = 60 +``` + +In the above, we use the square bracket `[]` to indicate a list of data. We will talk about list in future lessons. For now, just take not that a list is a collection data type that you can use to group similar data. Notice that we put only integers inside the list. This is part of what we have already indicated in our problem statement. PCDIT framework is not meant to be linear. In the case that we realize that some of the elements are not integer, we should go back to the **P**roblem statement step and revise it. + +In **C**oncrete Cases, we walk through the computation as if we are the computers. This is an important exercise of computational thinking. This means that we have to think like a computing agent in doing this step. Let's do it. + +The first step is to compute the average. In order to compute the average from the input, we need to get two things: the sum of all the elements and the number of elements in the list. + +Let's do the first step of getting the sum of all the elements first. We will need something to hold the sum, let's name it total. + +``` +total = 0 +``` + +Now, we go through every element in the list and add them to total. In the first iteration, we will take the value of the first element. + +``` +cadence_list = [45, 57, 62, 58, 55, 66, 63] + ^ + | +``` + +The value is `45` and we will add it to total. + +``` +total = 0 + 45 = 45 +``` + +Then we move to the next element. + +``` +cadence_list = [45, 57, 62, 58, 55, 66, 63] + ^ + | +``` + +And we add it again to total. + +``` +total = 45 + 57 = 102 +``` + +Similarly, to the third element. We do this until it reaches the last element. + +``` +cadence_list = [45, 57, 62, 58, 55, 66, 63] + ^ + | +total = 102 + 62 = 164 +``` +fourth iteration: +``` +cadence_list = [45, 57, 62, 58, 55, 66, 63] + ^ + | +total = 164 + 58 = 222 +``` + +fifth iteration: +``` +cadence_list = [45, 57, 62, 58, 55, 66, 63] + ^ + | +total = 222 + 55 = 277 +``` + +sixth iteration: +``` +cadence_list = [45, 57, 62, 58, 55, 66, 63] + ^ + | +total = 277 + 66 = 343 +``` + +seventh iteration: +``` +cadence_list = [45, 57, 62, 58, 55, 66, 63] + ^ + | +total = 343 + 63 = 406 +``` + +Now, we have reached the last element and get the sum of all the cadence values. We can get the average by dividing this total with the number of elements. There are seven elements and so we have: + +``` +average_cadence = 406 / 7 = 58 +``` + +Now we have the average cadence, we can *check* whether the average cadence hits the target value or not. + +If `average_cadence` is greater than or equal to the `target_cadence`, then the chatbot will display `"Good job, you hit your target for the past week cycling."`. Otherwise, we want to check the difference. If the difference is greater or equal to `10`, then we will request the user to adjust the target value. If the difference is less than `10`, we will just display, `"You almost hit your target, try harder in the coming weeks."`. + +In our concrete case here, our average cadence is 58 while the target cadence is 60. So in this case, we do not hit the target. Now, we need to calculate the difference. + +``` +difference = 60 - 58 = 2 +``` + +Since the difference is less than 10, we will just display `"You almost hit your target, try harder in the coming weeks."`. + +You should try with a different input values. For example, if the target is 70. + +``` +target_cadence = 70 +``` + +In this case, the difference will be: + +``` +difference = 70 - 58 = 12 +``` + +Since the difference is greater than 12, the chatbot should request the user to adjust the target. + +On the other hand, if the target is 50. + +``` +target_cadence = 50 +``` + +In this case, our average is greater than the target and we will just display, `"Good job, you hit your target for the past week cycling."` + +### Design of Algorithm + +Now, we are ready to design our algorithm. The word algorithm refers to the steps of the computation. We can derive the algorithm from the previous **C**oncrete Case steps by **generalizing** the steps we took in our computation. We will do so in two ways. The first one is through a pseudocode and the second one will use a flowchart. + +Let's start with using the pseudocode. A pseudocode is not a computer code. That's where the word *pseudo* comes from. It is meant to be general enough in such a way that it can be implemented by any programming language. The key feature of a pseudocode is to help us to think through about the steps without worrying about the programming language features. In these notes, we will use simple English for our pseudocode which we will then refine to use with certain key words that helps us to identify the structure of steps. + +We can divide the steps into few distinct stages: +- calculating the total +- calculating the average +- determining the state output +- displaying the state output + +To calculate the total, we start with an empty storage, `total = 0`. We then go through every element in the list and add the value to the `total`. We will write it in two stages. Here is our first draft. + +``` +1. Initialize *total* to 0. +2. Go to the first element. +3. Get the value of the first element and add to *total*. Store the result back to *total*. +4. Go to the second element. +5. Get the value of that element and add to *total*. Store the result back to *total*. +6. Go to the third element. +7. Get the value of that element and add to *total*. Store the result back to *total*. +8. Repeat the last two steps up to the last element. +``` + +There you go. We basically write what we have done in the **C**oncrete Cases steps. Notice that we repeat some steps again and again. This is an indicator of an **iterative** structure. What we want is to go through some steps **for every element in the list**. We can refine this further by using this keyword *for every element in the list*. + +``` +1. Initialize *total* = 0. +2. *for every element in the list* + 2.1. Add the value of that element to *total* and store back the result to *total*. +``` + +We have shortened our previous steps by using the *iterative* keyword *for every element in the list*. The steps are still easy to read and understood. Now, we can calculate the average. + +``` +1. Divide the *total* by the number of element in the list and store it to *average_cadence*. +``` + +In order to do the above, we need to know the number of element in the list. Let's combine these steps and re-write it. +``` +1. Initialize *total* to 0. +2. Get the number of element in the list and store it to *n*. +3. *for every element in the list* + 3.1. Add the value of that element to *total* and store back the result to *total*. +4. Divide *total* by *n* and store it to *average_cadence*. +``` + +Now, we can proceed to the next two stages that is to determine the state and display it. To determine the state, we can write something like the following. + +``` +1. if the *average_cadence* is greater than or equal to *target_cadence* + 1.1 Display "Good job, you hit your target for the past week cycling.". +2. Otherwise, + 2.1 calculate difference betwen *average_cadence* and *target_cadence*. + 2.2 Determine state depending on the difference + 2.2.1 if the difference is greater than 0 + 2.2.1.1 Display, "Good job, you hit your target." + 2.2.2 otherwise, if the difference is greater than -10 + 2.2.2.1 Display, "You almost hit your target, try harder in the coming weeks." + 2.2.3 Otherwise, + 2.2.3.1 Call *modify_target_cadence* function. +``` + +Let's combine all the steps now into one single algorithm. + +``` +1. Initialize *total* to 0. +2. Get the number of element in the list and store it to *n*. +3. *for every element in the list* + 3.1. Add the value of that element to *total* and store back the result to *total*. +4. Divide *total* by *n* and store it to *average_cadence*. +5. Determine what to display + 5.1 if the *average_cadence* is greater than or equal to *target_cadence* + 5.1 Display "Good job, you hit your target for the past week cycling.". + 5.2 Otherwise, + 5.2.1 calculate difference betwen *average_cadence* and *target_cadence*. + 5.2.2 if the difference is greater than 0 + 5.2.2.1 Display, "Good job, you hit your target." + 5.2.3.1 otherwise, if the difference is greater than -10 + 5.2.3.1.1 Display, "You almost hit your target, try harder in the coming weeks." + 5.2.4.2 Otherwise, + 5.2.4.2.1 Call *modify_target_cadence* function. +``` + +Notice a few things: +- In general, the structure is sequential which means the instruction is executed from the top to the bottom. We need to do the earlier steps before the later steps. This is true for all computer codes. +- Starting with the total calculation, we observe there is an iterative structure. We identify this iterative structure because in the previous draft of our design of algorithm we have some repeated steps that we do *for every element in the list*. We then use this keyword *for every element in the list* in our second draft of the design of algorithm. +- The step that is being repeated in this iterative structure is simply to add the element in the list to the total. We indented this step and added a sub numbering to indicate that this step is part of the iterative structure. +- In step 5, we can observe the branch structure. In fact, this branch structure has another branch structure inside it in step 5.2.2. All structure can be nested inside another structure. + +We can also represent the above design of algorithm using a Flowchart. 5.2.2. All structure can be nested inside another structure. + +We can also represent the above design of algorithm using a Flowchart. We can iterate the flowchart from big steps to the smaller steps. For example, we can start with the following basic steps. + + + +Notice, that we do not describe in detail on how to get the average. We can expand the process to calculate the average using the following flowchart. + + + +Notice that the iterative structure in the above flowchart. We continue adding as long as there is a next element in the list. When there is no more next element, we stop the iteration and calculate the average. + +Similarly, we can draw the flowchart to determine the state of the program after the average cadence calculation as shown below. + + + + +Notice that we also have two decision box in this section showing how the branch structure is actually nested inside another branch structure. + +Finally, we can combine the different part into one single flowchart as shown below. + + + +For smaller problem like the above, we can draw all the parts in a single flowchart. For bigger problems, we may need to modularize and separate the flowchart into different sections and parts. + +## Summary + +In this section, we introduced to you the three basic structures, sequential, branch and iterative. The basic structure is sequential. However, the branch structure actually is the one that creates flexibility in our computer programs as it allows us to choose what to do depending on some conditions. In fact, the iterative structure is based on the branch structure as it chooses to repeat certain block of steps depending on some conditions. + +We also introduce how you can write down your **C**oncrete Cases and **D**esign of Algorithm. We showed you how to iterate over your design of algorithm using both pseudocode and flowchart. In subsequent lessons, we will focus on pseudocode to design our algorithm. \ No newline at end of file diff --git a/_Notes/BinaryHeap_Heapsort.md b/_Notes/BinaryHeap_Heapsort.md deleted file mode 100644 index 41b4089..0000000 --- a/_Notes/BinaryHeap_Heapsort.md +++ /dev/null @@ -1,470 +0,0 @@ ---- -title: Binary Heap and Heapsort -permalink: /notes/binary_heap_sort -key: notes-sort-binaryheap -layout: article -nav_key: Notes -sidebar: - nav: Notes -license: false -aside: - toc: true -show_edit_on_github: false -show_date: false ---- - -Previously, we discussed two sorting algorithms called [Bubble Sort and Insertion Sort]({% link _Notes/BubbleSort_InsertionSort.md %}). In this section we will apply our programming skills to investigate another sorting algorithm called the Heapsort. We will then compare the performance of Heapsort with the previous [Bubble Sort and Insertion Sort]({% link _Notes/BubbleSort_InsertionSort.md %}). We will discuss some notations on how to analyze these performance. - -One reason why we introduce different sorting algorithms is to show you that there are many ways to solve the same problems. At the same time, these different ways may have different performances. After we introduce binary heap and heapsort algorithm, we will begin to introduce you how to analyze these different algorithms in terms of computation time. You will notice that Heapsort algorithm is a much better sorting algorithm as compared to Bubble sort and Insertion sort algorithms. - -## Binary Heap - -Before, discussing Heapsort algorithm, we have to introduce a new data structure called _binary heap_ or simply called _heap_. The heap is an array of object that we can view as a nearly perfect binary tree (or we can call it a _complete_ binary tree). - -> A perfect binary tree is a full binary tree which **all** leaf nodes are at the same level. Complete binary trees are nearly perfect **except** the last level and all the leaves at the last level are packed towards the left. - -You are familiar with the concept of array. But what is a binary tree? The easiest way to explain it is using some examples. The image below shows you an array of integers. - -![Array of Integers Representing Binary Heap](/assets/images/week2/Binary_Heap_Array.png) - -We have indicated the indices of each element in the array, which starts from 0. We can visualize the elements in this array in a form of a _tree_ as shown below. - -![Binary Tree Satisfying Heap Property](/assets/images/week2/Binary_Heap.png) - -A _tree_ in computer science is up-side down. It consists of _nodes_ and it has one _root_ node, which is at the top. In a _binary_ tree, each node has only _two_ children, which we will call the _left_ child and the _right_ child. Every node, except the root, has a _parent_ node. The node without children is what we called a _leaf_. - -Let's take a look at the example above and put in all the terms we have mentioned: - -- In the above tree, we have 10 nodes, where each node is each element in the array. -- The _root_ node is the element 16, which has an index 0 in the array. -- Node with element 16 (root node) has two children. The _left_ child is a node with the element 14, while the _right_ child is a node with the element 10. -- Elements 9, 3, 2, 4, and 1 are all _leaves_ because they do not have any children. -- The node with element 7 (index 4) has only one child, which is the node with element 1 (index 9). -- The node with element 1 (index 9) has node with index 4 as its _parent_. - -Now, let's go back to our definition of a _heap_. The heap is an array of object that we can view as a nearly perfect binary tree. - -- A binary tree is a tree where all the nodes have only a maximum of two children, which you can call _left_ child and _right_ child. -- A _full_ binary tree is a tree where all the nodes except the leaves have _two_ children. In our case, heap is a _complete_ binary tree and not a _full_ binary tree. A _complete_ binary tree is a binary tree in which every level, except possibly the last, is _completely filled_, and all nodes are as far left as possible. A _complete_ binary tree is similar to a full binary tree with two major differences: all the leaf elements must lean toward the left and the last leaf element may not have the right sibling. -- The _root_ of the tree is the node with index 0. -- We put the elements of our array into our _tree_ from top to bottom and left to right sequence. With this, we can calculate the index of the children and the parent for every node. - -### Indices of Children and Parent in a Heap - -Let's start by considering how to calculate the index of the parent node. Let's take a look at the example tree we have considered. - -![Binary Tree Satisfying Heap Property](/assets/images/week2/Binary_Heap.png) - -Note the following: - -- The parent of node index 1 is 0. Similarly, the parent of node index 2 is also 0. -- The parent of node index 3 and 4 is 1, while the parent of node index 5 and 6 is 2. - -How do we get index 0 from both indices 1 and 2? And how do we get index 1 from indices 3 and 4? Or index 2 from indices 5 and 6? - -``` -def parent(index): -Input: index of current node -Output: index of the parent node -Steps: -1. return integer((index-1) / 2) -``` - -We can test the above pseudocode. $(1-1)/2 = 0$ and $(2-1)/2 = 0$ for the second level nodes. Similarly, $(3-1)/2 = 1$ and $(4-1)/2 = 1$ for the third level nodes with the parent index 1. And $(5-1)/2 =2$ together with $(6-1)/2= 2$ for the third level nodes with the parent index 2. Note that we use _integer_ division to get the correct parent node. - -In order to get the index of the _left_ child, let's observe the following: - -- The left child of node 0 is index 1. -- The left child of node 1 is index 3. -- The left child of node 2 is index 5. -- and so on. - -We can get the left child index using the following: - -``` -def left(index): -Input: index of current node -Output: index of the left child node -Steps: -1. return (index * 2) + 1 -``` - -Test the above pseudocode and ensure it gives the correct left child nodes. - -Similarly, we can observe the following for the indices of the _right_ child nodes: - -- The right child of node 0 is index 2. -- The right child of node 1 is index 4. -- The right child of node 2 is index 6. -- and so on. - -We can get the right child index using the following: - -``` -def right(index): -Input: index of current node -Output: index of the right child node -Steps: -1. return (index + 1) * 2 -``` - -You can verify the above code yourself. - -### Heap Property - -There are two kinds of _heap_: max-heaps and min-heaps. In this case we will discuss only _max-heaps_. Both heaps must satisfy the **heap property**, which specifies the kind of heap we are dealing with. The **max-heap property** is specified as follows: - - For all nodes except the root: - - A[parent(i)] >= A[i] - -This means that in a max-heap, the parent nodes are always greater that their children. This also implies that the largest node is at the _root_. The figure below is an example of a max-heap because it satisfies the condition above. - -![Binary Tree Satisfying Heap Property](/assets/images/week2/Binary_Heap.png) - -### Heapify - Maintaining the Heap Property - -We will now describe an algorithm on how to maintain the _heap property_ and in this example is the _max-heap property_. We will call this procedures to maintain the _max-heap property_ as max-heapify. The idea is that for a given node, we will push down this node in such a way that the _max-heap property is satisfied_. This assumes that the _left_ child of the given node forms a tree that satisfies _max-heap property_. Similarly, the _right_ child of the given node forms a tree that satisfies _max-heap property_. The only part that does not satisfy the _max-heap property_ is the current node and its two children. - -#### (P)roblem Statement - -Given an index of a node in a binary tree, where the left and the right children of that node satisfies the _max-heap property_, restore the _max-heap property_ of the tree starting from the current node. - - Input: index of the current node in a heap - Output: None - Process: re-order the elements in the heap - in such a way that the max-heap property is satisfied - from the current index node. - assumption: - - left child forms a tree that satisfies max-heap property - - right child forms a tree that satisfies max-heap property - - current node with its children may not satisfy max-heap property - -#### Test (C)ase - -Let's take a look at the tree below. - -![](/assets/images/week2/Heapify_1.png) - -Note the following: - -- The current node is index 1, which has the element of 4. -- The current with its children does not satisfy the _max-heap_ property because $4 < 14$ and $4 < 7$. -- The left child, i.e. tree starting from index 3 (elements 14, with children of 2 and 8), forms a tree that satisfies the _max-heap property_. -- The right child, i.e. tree starting from index 4 (element 7, with one children of 1), forms a tree that satisfies the _max-heap property_. - -The procedure of _max-heapify_ will push the current node by swapping with the largest node along the way to satisfy the _max-heap property_. To do that, in the process of pushing the nodes, we will swap that node with the _largest_ child. In this way, we satisfy the _max-heap property_. - -Let's look at the particular example above. Given the tree above, we do the folloing: - -- We first find the largest child of the current node. The current node is element 4 (index 1). The largest child is element 14 (index 3), which is the left child of the current node. - - ![](/assets/images/week2/Heapify_1.png) - -- We then swap the current node with the largest child, i.e. element 4 (index 1) with element 14 (index 3). - - ![](/assets/images/week2/Heapify_2.png) - -- We, then, move our current index to the place where we swap, i.e. old index of element 14. So we are now at index 3. -- We do the same thing by looking if any of the children is larger than the current node. Since $8 > 4$, we swap 4 (index 3) with 8 (index 8). - - ![](/assets/images/week2/Heapify_3.png) - -- We, then, move our current index to the place where we swap, i.e. old index of element 8. So we are now at index 8. -- Since this node has no more children, we stop. We can check whether the node has more children by calculating the index of the _left_ child and see if it is still within the length of the array minus one, i.e. $left(i) < n-1$, where $i$ is the current node index and $n$ is the number of element in the array. - -#### (D)esign of Algorithm - -We can write down the steps we did in the previous section as follows. - -``` -def max-heapify(A, i): -version: 1 -Input: - - A = array storing the heap - - i = index of the current node to restore max-heap property -Output: None, restore the element in place -Steps: -1. current_i = i # current index starting from input i -2. As long as ( left(current_i) < length of array), do: - 2.1 max_child_i = get the index of largest child of the node current_i - 2.2 if array[max_child_i] > array[current_i], do: - 2.2.1 swap( array[max_child_i], array[current_i]) - 2.3 current_i = max_child_i # move to the index of the largest child -``` - -Note that the above steps will continue iterating down even if the current node already satisfies _max-heap_ property. This means that we can stop iterating if the largest children is already less than the current node. We can do this by checking if any swap is happening. If no swap is needed then we are done. This is because we assumes that the left child and the right child already satisfies _max-heap property_. - -``` -def max-heapify(A, i): -version: 2 -Input: - - A = array storing the heap - - i = index of the current node to restore max-heap property -Output: None, restore the element in place -Steps: -1. current_i = i # current index starting from input i -2. swapped = True -3. As long as ( left(current_i) < length of array) AND swapped, do: - 3.1 swapped = False - 3.2 max_child_i = get the index of largest child of the node current_i - 3.3 if array[max_child_i] > array[current_i], do: - 3.3.1 swap( array[max_child_i], array[current_i]) - 3.3.2 swapped = True - 3.3 current_i = max_child_i # move to the index of the largest child -``` - -Note: - -- We introduced a boolean variable called `swapped`. At every iteration, we set `swapped` to `False`. -- If there is a swap, we set this boolean variable to `True` and continues to the next iteration. -- If there is no swap, the boolean variable is still `False` and so it will stop the iteration. - -### Building a Heap - -We can then use the previous procedure _max-heapify_ to build a _binary heap_ data structure from any arbitrary array. The idea is to go through every nodes in the tree and _heapify_ them. However, we need not do for all the nodes, but rather only _half_ of those nodes. The reason is that we do not need to heapify the _leaves_. - -We can show that the elements in the array from index $n/2$ to $n-1$ are all leaves. We do not need to push down these nodes as they do not have any children. So we can stry from element at position $n/2 - 1$ and move up to element at position 0. - -#### (P)roblem Statement - -Given an arbitrary array, re-order the elements in such a way that it satisfies _max-heap property_. - -``` -Input: any arbitrary array of integers -Output: None -Process: Re-order the elements such that the whole array satisfies max-heap property -``` - -#### Test (C)ases - -Let's consider an array as shown below. - -[1, 2, 8, 7, 14, 9, 3, 10, 4, 16] - -- We first visualize this array as a binary tree as shown below. Note that this tree does not satisfy _max-heap property_. - - ![](/assets/images/week2/Build_Heap_1.png) - -- We will start from the middle index, i.e. $n/2 - 1 = 10/2 - 1 = 4$, which is the fifth element, i.e. 14. Notice that all the elements after 14 are all _leaves_. We call _max-heapify_ on 14 and the result is a swap between 14 and 16. We only have one iteration because now 14 has reached the end of the array and cannot be compared with any other nodes. In the figure below, we indicate the next element to consider with a _dotted_ circle. - - [1, 2, 8, 7, **14**, 9, 3, 10, 4, 16] - - ![](/assets/images/week2/Build_Heap_2.png) - - [1, 2, 8, 7, **16**, 9, 3, 10, 4, **14**] - -- Now we move to the element on the left of 16, which is 7. The result of _max-heapify_ will swap 7 with 10. - - [1, 2, 8, **7**, 16, 9, 3, 10, 4, 14] - - ![](/assets/images/week2/Build_Heap_3.png) - - [1, 2, 8, **10**, 16, 9, 3, **7**, 4, 14] - -- Now, we move to the next element, which is 8. The result of _max-heapify_ will swap 8 with 9. - - [1, 2, **8**, 10, 16, 9, 3, 7, 4, 14] - - ![](/assets/images/week2/Build_Heap_4.png) - - [1, 2, **9**, 10, 16, **8**, 3, 7, 4, 14] - -- We move on to the next element, which is 2. The result of _max-heapify_ will swap 2 with 16, and then 2 with 14. - - [1, **2**, 9, 10, **16**, 8, 3, 7, 4, 14] - - ![](/assets/images/week2/Build_Heap_5.png) - - [1, **16**, 9, 10, **2**, 8, 3, 7, 4, 14] - - and then, - - [1, 16, 9, 10, **2**, 8, 3, 7, 4, **14**] - - ![](/assets/images/week2/Build_Heap_6.png) - - [1, 16, 9, 10, **14**, 8, 3, 7, 4, **2**] - -- And now we move to the last element, which is 1. The result of _max-heapify_ will swap 1 with 16, and then 1 with 14, and finally 1 with 2. - - [**1**, **16**, 9, 10, 14, 8, 3, 7, 4, 2] - - ![](/assets/images/week2/Build_Heap_7.png) - - [**16**, **1**, 9, 10, 14, 8, 3, 7, 4, 2] - - next, - - [16, **1**, 9, 10, **14**, 8, 3, 7, 4, 2] - - ![](/assets/images/week2/Build_Heap_8.png) - - [16, **14**, 9, 10, **1**, 8, 3, 7, 4, 2] - - lastly, - - [16, 14, 9, 10, **1**, 8, 3, 7, 4, **2**] - - ![](/assets/images/week2/Build_Heap_9.png) - - [16, 14, 9, 10, **2**, 8, 3, 7, 4, **1**] - -- Once it reaches the last element, the whole array now satisfies the _max-heap property_. - -#### (D)esign of Algorithm - -We can then write down the steps in a pseudocode as follows: - -``` -def build-max-heap(array): -Input: - - array: arbitrary array of integers -Output: None, sort the element in place -Steps: -1. n = length of array -2. starting_index = integer(n / 2) - 1 # start from the middle or non-leaf node -3. For current_index in Range(from starting_index down to 0), do: - 3.1 call max-heapify(array, current_index) -``` - -Note: - -- The pseudo simply says, we start from the middle node as our `starting_index`, and call the function max-heapify on that node. -- We then move to the left and continues calling max-heapify until we reach the first element at index 0. - -## Heapsort - -Now, we can consider Heapsort algorithm. The idea of a heapsort is pretty simple. For any arbitrary array, we can sort the integers in the array by first building a _max-heap_. Once the max-heap is built, we know that the maximum is at the _root_ node. With this, we can swap the _root_ node with the last element and then exclude it from our heap. We then should restore the _max-heap property_ after this swap because now the _root_ node will be a small number. We can do this repetitively until there is no more element in the heap. - -### (P)roblem Statement - -Given an arbitrary array of integers, sort the element using heapsort algorithm. - -``` -Input: array of integers -Output: None -Process: Sort the elements of the array in place using heapsort -``` - -### Test (C)ases - -Let's use the same example as in the previous seciton. Let's say we have the following array. - -[1, 2, 8, 7, 14, 9, 3, 10, 4, 16] - -We will sort the elements following these steps: - -- Build a max-heap from this array. The previous section has shown that the final output of building a max-heap will be: - - [16, 14, 9, 10, 2, 8, 3, 7, 4, 1] - -- Now, we will swap the largest element with the last element, and exclude it from the heap. We will put the excluded element in what we called as `sorted`. - - heap = [**1**, 14, 9, 10, 2, 8, 3, 7, 4], sorted = [**16**] - -- Notice, now, that the array does not satisfy the _max-heap property_. So we must _max-heapify_ the array to push the element 1 down to its place. The process of _max-heapify_ from the root node will result in: - - heap = [**1**, **14**, 9, 10, 2, 8, 3, 7, 4], sorted = [16] - - heap = [**14**, **1**, 9, 10, 2, 8, 3, 7, 4], sorted = [16] - - heap = [14, **10**, 9, **1**, 2, 8, 3, 7, 4], sorted = [16] - - heap = [14, 10, 9, **7**, 2, 8, 3, **1**, 4], sorted = [16] - -- Once we have restored the _max-heap property_, we can take out the largest element from the first element and swap it with the last element in the heap. - - heap = [**4**, 10, 9, 7, 2, 8, 3, 1], sorted = [**14**, 16] - -- We then _max-heapify_ the heap again to restore the _max-heap property_. - - heap = [**4**, **10**, 9, 7, 2, 8, 3, 1], sorted = [**14**, 16] - - heap = [**10**, **4**, 9, 7, 2, 8, 3, 1], sorted = [**14**, 16] - - heap = [10, **7**, 9, **4**, 2, 8, 3, 1], sorted = [**14**, 16] - -- We then swap the largest element with the last element in the heap, and take it out from the heap. - - heap = [**1**, 7, 9, 4, 2, 8, 3], sorted = [**10**, 14, 16] - -- The same process of _max-heapify_ happens again. We will now _remove_ the intermediate step and only show the first and the final state of the heaps. - - heap = [**1**, 7, 9, 4, 2, 8, 3], sorted = [**10**, 14, 16] - - heap = [9, 7, 8, 4, 2, **1**, 3], sorted = [**10**, 14, 16] - -- We swap and take out again the largest element. The next iteration would be: - - heap = [**3**, 7, 8, 4, 2, **1**], sorted = [**9**, 10, 14, 16] - - then we max-heapify the array: - - heap = [8, 7, **3**, 4, 2, 1], sorted = [**9**, 10, 14, 16] - - Swapping and taking out the largest element: - - heap = [**1**, 7, 3, 4, 2], sorted = [**8**, 9, 10, 14, 16] - - and max-heapify: - - heap = [7, 4, 3, **1**, 2], sorted = [**8**, 9, 10, 14, 16] - - Swapping and taking out the largest element: - - heap = [**2**, 4, 3, 1], sorted = [**7**, 8, 9, 10, 14, 16] - - and max-heapify: - - heap = [4, **2**, 3, 1], sorted = [**7**, 8, 9, 10, 14, 16] - - Swapping and taking out the largest element: - - heap = [**1**, 2, 3], sorted = [**4**, 7, 8, 9, 10, 14, 16] - - and max-heapify: - - heap = [3, 2, **1**], sorted = [**4**, 7, 8, 9, 10, 14, 16] - - Swapping and taking out the largest element: - - heap = [**1**, 2], sorted = [**3**, 4, 7, 8, 9, 10, 14, 16] - - and max-heapify: - - heap = [2, **1**], sorted = [**3**, 4, 7, 8, 9, 10, 14, 16] - - Swapping and taking out the largest element: - - heap = [**1**], sorted = [**2**, 3, 4, 7, 8, 9, 10, 14, 16] - -- At this point in time, the array is already sorted. If `heap` and `sorted` are not a separate array but rather one single array, we will have: - - result = [1, 2, 3, 4, 7, 8, 9, 10, 14, 16] - -### (D)esign of Algorithm - -Let's write down the steps in the previous section in a pseudocode. - -``` -def heapsort(array): -Input: - - array: any arbitrary array -Output: None -Steps: -1. call build-max-heap(array) -2. heap_end_pos = length of array - 1 # index of the last element in the heap -3. As long as (heap_end_pos > 0), do: - 3.1 swap( array[0], array[heap_end_pos]) - 3.2 heap_end_pos = heap_end_pos -1 # reduce heap size - 3.3 call max-heapify(array[from index 0 to heap_end_pos inclusive], 0) -``` - -Note: - -- We first call the procedure in the previous section called `build-max-heap` to create the _max-heap_ data structure. -- We then start from the last element in the heap and swap it with the largest element (always at index 0). -- We reduce the variable `heap_end_pos` to reduce the heap size and exclude the largest element from the heap. -- Then, we can call `max-heapify` on a subarray. The subarray starts from index 0 of the current array up to index `heap_end_pos`. In this way, we exclude the largest element from being _max-heapified_. -- The second argument of `max-heapify` is the starting node where the process should begins. In this case, we always want to start `max-heapify` from index 0 because this is the node where we replace the largest element with some small element from the end of the heap. diff --git a/_Notes/Boolean_Data.md b/_Notes/Boolean_Data.md new file mode 100644 index 0000000..f045cc2 --- /dev/null +++ b/_Notes/Boolean_Data.md @@ -0,0 +1,384 @@ +--- +title: Boolean Data +permalink: /notes/boolean-data +key: notes-boolean-data +layout: article +nav_key: Notes +sidebar: + nav: Notes +license: false +aside: + toc: true +show_edit_on_github: false +show_date: false +--- + +## True or False? +In our previous lessons, we mentioned that the branch structure is the one that gives a computer program flexibility as it allows us to do various things depending on some conditions. This also means that the branch structure depends on being able to check those conditions. + +Recall in our cadence target example in the previous lesson that we want to determine what the chatbot should do depending on whether the average cadence hits the target that the user has set or not. Whether the user hits the target or not is what we call as **Boolean** data. + +Boolean data only has two values, either it is **True** or **False**. In our example, it is whether the user hits the target (true) or it does not hit the target (false). The word Boolean comes from a Mathematician with the name [George Boole](https://en.wikipedia.org/wiki/George_Boole) who devised the boolean algebra. + +As with any other data type, you should ask, "how do we create this data?". Similarly, we should ask, how we can create Boolean data. Since Boolean data is either True or False, we usually create them through some operators. There are two kind of operators that we use to create boolean data: relational operators and logical operators. + +## Relational Operators + +Relational operators relates two values and results in a boolean value indicating whether the relation is True or False. Below is a table of some of the common relational operators. + +| operator | checking | +|----------|------------------------------------| +| a == b | if a is equal to b | +| a != b | if a is not equal to b | +| a < b | if a is less than b | +| a <= b | if a is less than or equal to b | +| a > b | if a is greater than b | +| a >= b | if a is greater than or equal to b | + +Notice that the operator to check equality is `==` and not `=`. Recall that `=` is the assignment operator. Notice also that for checking if two values are not equal it uses `!=`. You will see the `!` operator in the logical operator again but it will have a different meaning. + +But now, let's see how we can generate boolean data from these operators. Below are some examples. + +```python +>>> 3 == 4 +False +>>> 4 == 4 +True +>>> 3 != 4 +True +>>> 4 != 4 +False +>>> 3 < 4 +True +>>> 4 <= 4 +True +>>> 3 > 4 +False +>>> 4 >= 4 +True +``` + +You can use this relational operator not only with integer but also with float. You can also do so with string data. However, string data comparison is rather tricky. Let's see a few example. + +```python +>>> 'hello' == 'hello' +True +>>> 'Hello' == 'hello' +False +``` + +We can use the equality operator to compare two strings. However, this comparison is case sensitive. In fact, string comparison takes into acount the whitespaces in the string. See the following comparison. + +```python +>>> 'abc' == ' abc ' +False +``` + +We will discuss on future lessons on what we should do to process string before we compare them. But for now, you need to remember that string comparison is: +- case sensitive +- takes into account the non-visible character such as whitespaces + +The relational operator such as less than or greater than is also tricky. Below is a few example of what we can expect when comparing a single character string. + +```python +>>> 'a' < 'b' +True +>>> 'b' < 'c' +True +>>> 'a' < 'z' +True +``` + +However, the below comparison may surprise some of you. + +```python +>>> 'a' < 'A' +False +>>> 'a' < 'B' +False +>>> 'a' < 'Z' +False +``` + +All the evaluated results are false in the three comparison above. The reason is that the way Python compares two strings is by comparing its ASCII numbering of the characters. You can see a list of ASCII numbering for different characters in [this website](https://www.asciitable.com). But notice that small letter `a` has an ASCII number of 97 while the capital letter `A` has an ASCII number er of 65. In fact, all the capital letters has a lower ASCII numberings than the small letters. That's the reason why all the above comparisons result in False. + +How about comparing two strings that has more than one letter? Let's see a few results below. + +```python +>>> 'abc' < 'aab' +False +>>> 'abc' < 'abd' +True +>>> 'abc' < 'abcd' +True +>>> 'abd' < 'abcd' +False +``` + +What can we say about those results? +- Python compares the two strings using the ASCII numbering of the characters. +- Python compares the two strings character by character from left to right. For example, `'abc' < 'aab'` because the second character comparison is false, i.e. `'b' > 'a'`. You can see this also from the second example when `'d' > 'c'`. +- When the substring of the prefix of the string are the same, the longer string has a greater value. Most probably because a NULL character has an ASCII number of 0. You can see this from the third example where `'abc' < 'abcd'`. In this case all the first three characters are the same. The first string only has three characters but the second string has an additional character `d`. In this way, it is like comparing the ASCII of `NULL < 'd'` which results in True. +- Since Python compares the two strings character by character from left to right, once one of the character is greater than the other, it will produce False. This is seen in the last example `'abd' < 'abcd'`. In this case, the first two characters are the same. However, the third comparisons check if `'d' < 'c'`. The result of this is False and it will stop comparing the rest of the characters. + +Python supports relational operators for all its built-in data types. This means that you can actually use the relational operators to Boolean data as well. Again some of this result may be intuitive and some may not. + +```python +>>> True == True +True +>>> True < True +False +>>> True < False +False +>>> False < True +True +``` + +The first two results are intuitive enough. True is equal to True and since they are equal, they cannot be less than the other. The last two may not be so intuitive. Here, we have `True < False` which results in False and `False < True` which results in True. How can we understand this? + +One way to remember this is that Python encodes `False` to an integer `0` and `True` to an integer `1`. You can verify this. + +```python +>>> 0 == False +True +>>> 1 == True +True +``` + +On top of that Python is able to convert all its other built-in data type into Boolean data. This means that certain values are considered as False while the rest is considered as True. Let's see some of them. We are going to use the `bool()` function to convert these other data to boolean data. + + +```python +>>> bool(0) +False +>>> bool(1) +True +>>> bool(2) +True +>>> bool(-1) +True +>>> bool(0.0) +False +>>> bool(0.1) +True +>>> bool('') +False +>>> bool('False') +True +``` + +Notice that for integer values, 0 is considered as `False` and all other integers are considered as `True`. Similarly, for float, `0.` is considered as `False` and all other float values is considered as `True`. As about string data, an empty string `''` is considered as `False` and all other non-empty string is considered as `True`. + +Notice, however, that though empty string is considered as a False value, it is not equal to False. Only 0 and 0. is equal to False. + +```python +>>> '' == False +False +``` + +## Logical Operators + +On top of relational operators, logical operators also evaluate to boolean data. There are three common logical operators as shown in the table below. + +| operator | remarks | +|----------|--------------------------------------------------------------| +| a and b | true if both a and b are true | +| a or b | true if at least one of the operands (either a or b) is true | +| not a | true if a is false and false if a is true | + +In the above table, `a` and `b` can be any other expressions that can be evaluated to boolean data. This means that we can put some relational operators in either `a` and `b`. Let's see some examples below. + +```python +>>> 3 < 4 or 3 > 5 +True +>>> 3 < 4 and 3 > 5 +False +``` + +The first one is `True` because `3 < 4` evaluates to `True`. And for `or` operator to return `True` it only requires at least one of the operands to be `True`. On the other hand `and` operator requires both operands to be `True` to result in a `True` value. + + +```python +>>> 3 == 3 and 3 < 4 +True +``` + +Python actuall supports using to relational operators at the same time assuming it is related by the `and` operator. See below example. + +```python +>>> 2 < 3 and 3 < 4 +True +>>> 2 < 3 < 4 +True +``` + +The two expressions above are the same. + +The last logical operator always invert the boolean values. + +```python +>>> not True +False +>>> not False +True +>>> not 0 +True +>>> not 1 +False +``` + +You can combine all the relational and logical operators together. It is recommended to use parenthesis to clarify which order we want the operations to be evaluated. + +```python +>>> (3 < 4) or (5 < 1) and True +True +>>> not( (3 < 4) or (5 < 1) and True) +False +``` + +## Evaluating Boolean Expressions + +As with the arithmetic operators, relational and logical operators also have their precedence. This determines which operator to be evaluated first. It is important to know this as it may help us greatly in debugging our code. + +Consider the below example. + +```python +>>> (1 < 5) or (3 > 4) and False +True +``` + +At first glance, it may not be obvious why the result is True. +- `1 < 5` results in `True` +- `3 > 4` results in `False` + +The question now is whether we evaluate the `or` operator first or the `and` operator first. + +If we evaluate the `or` operator first, we will get `True or False` which results in `True`. Then we evaluate the `True and False` when evaluating the `and` operator which results in `False`. + +However, what happens is that `and` has a higher precedence than `or` and Python evaluates `(3 > 4) and False` first which results in `False`. Only then, Python evaluates the `or` operator: `(1 < 5) or False` which evaluates to `True or False` resulting in a `True` value. + +Similarly, we need to know whether `not` operator has a higher precedence as compared to `and` and `or` operator. Let's figure it out using some of these examples. + +```python +>>> not False or False +True +``` + +As can be seen above, `not` has higher precedence as compared to `or`. The reason is that `not False` is evaluated first which results in a `True` value. Only then Python evaluates `True or False` which results in a `True`. How about with regards to `and` operator? + +```python +>>> not False and True +True +``` + +Similarly, `not` operator is evaluated first than the `and` operator in the above example which results in evaluating `True and True`. + +How about between the relational and logical operators? Which is evaluated first? + +```python +>>> 3 < 0 and True +False +>>> 3 < 0 or 4 > 1 +True +``` + +In the first example, `3 < 0` is evaluated first which results in `False`. Then Python evaluates `False and True` to produce `False` as the output. In the second example, we also evaluate the relational operators first to produce `False or True` which results in `True`. + +Let's add on these precedence to our previous table. We now have the following. + +| precedence | operator | remarks | +|------------|----------|--------------------------------------------------------------------------------------------------------------------------------| +| 0 | () | Parenthesis is used to evaluate the inside expression to override the precedence of the surrounding operators. | +| 1 | ** | Power or exponentiation operator. | +| 2 | * / % // | Multiplication, division, modulus and integer division. They are evaluated from left to right when appearing in a single line. | +| 3 | + - | Addition and subtraction. | +| 4 | = | Assignment operator. | +| 5 | <, <=, >, >=, !=, == | Relational operators. | +| 6 | not | Boolean NOT operator. | +| 7 | and | Boolean AND operator. | +| 8 | or | Boolean OR operator. | + +Now, we know how to evaluate boolean data. We are not ready to use it in our program especially in the *branch* and *iterative* structure. Before we move on to the *branch* structure. There is one simple useful use of boolean data in programming and that is to **T**est. This is the last part of the PCDIT framework and the way we do it is to write test and implementation in small bites repetitively. + +## Testing Using Assert + +One simple way to test your code is to use the `assert` statement in Python. This is the general syntax to use `assert`. + +```python +assert condition +``` + +The way it works is that if the `condition` is `True`, Python will continue to the next line without complaining. However, if the `condition` is `False`, Python will throw an exception or an error. This is useful when writing codes because we can fill in our codes with assert statements to test it and our programme will continue running as it is as long as all those conditions are fulfilled. Only when there is an error, Python will stop and tell us at which `assert` statement that our test fails. Let's take a look at how we can make use of it in our PCDIT framework. + +Let's look at some of the functions we have created in the past lessons. One of the first function we created was `compute_cadence_for_30sec(steps)`. When we are trying to write this function, instead of starting with some implementation, we can start with some **expectation**. This can be written as a series of test. These expectations can be writtend based on our specifications for that problem. For example, in the function above, we expect that this function will caculate the cadence given the number of steps. We can write a few expected output before we write any implementations. + +```python +assert compute_cadence_for_30sec(10) == 20 +assert compute_cadence_for_30sec(21) == 42 +assert compute_cadence_for_30sec(25) == 50 +assert compute_cadence_for_30sec(31) == 62 +``` + +Notice that the expression on the right of the `assert` statement is a Boolean expression that makes use of the relational operator `==`. We wrote those expected output of the function based on our problem spefications and even our **C**oncrete Cases that we did during our PCDIT steps. From the above expectations, which is our **T**est, we can start writing our **I**mplementation. + +```python +def compute_cadence_for_30sec(steps): + return steps * 2 + +assert compute_cadence_for_30sec(10) == 20 +assert compute_cadence_for_30sec(21) == 42 +assert compute_cadence_for_30sec(25) == 50 +assert compute_cadence_for_30sec(31) == 62 +``` + +When you run the above code with `mypy` and `python`, you will see no complain thrown out. + +```sh +$ mypy 01_compute_cadence.py +Success: no issues found in 1 source file +$ python 01_compute_cadence.py +$ +``` + +You can try the above code in Python Tutor shown below. + + + +Notice that Python does not produce any output in the standard output since we do not have any `print()` statement. However, the program finishes without error. The reason that the program finishes is because our **I**mplementation fulfills all the `assert` statements which is our expectation. + +To see what happens if we have a wrong **I**mplementation, let's try changing our implementation purposely to introduce a wrong implementation. Let's use `**` instead of `*` in our calculation. + +```python +def compute_cadence_for_30sec(steps: int) -> int: + return steps ** 2 + +assert compute_cadence_for_30sec(10) == 20 +assert compute_cadence_for_30sec(21) == 42 +assert compute_cadence_for_30sec(25) == 50 +assert compute_cadence_for_30sec(31) == 62 +``` + +You can run the file `02_compute_cadence_error.py` with `mypy`. + +```sh +$ mypy 02_compute_cadence_error.py +Success: no issues found in 1 source file +``` + +Note that `mypy` does not complain as it does not find any error from the static check. However, running Python will throw an error. + +```sh +$ python 02_compute_cadence_error.py +Traceback (most recent call last): + File "02_compute_cadence_error.py", line 4, in + assert compute_cadence_for_30sec(10) == 20 +AssertionError +``` + +The error is of `AssertionError` and Python actually points out that it fails on the first `assert` statement as the output of the function is not equal to 20. + +You can also try it in Python Tutor and it will stop on the first `assert` statement. + + + +The `assert` statement is useful in driving our implementation. As shown in the above example, we can immediately know there is a bug in our implementation since our test fails. This is part of **Test Driven Development** (TDD). In TDD, we write our test before we write any implementation. As we go along these lessons, we will share more on how to write test that drives implementation. But for now, we can see how boolean data can be used for testing our implementation. \ No newline at end of file diff --git a/_Notes/Branch_Structure.md b/_Notes/Branch_Structure.md new file mode 100644 index 0000000..c87d2f4 --- /dev/null +++ b/_Notes/Branch_Structure.md @@ -0,0 +1,354 @@ +--- +title: Branch Structures +permalink: /notes/branch +key: notes-branch-structures +layout: article +nav_key: Notes +sidebar: + nav: Notes +license: false +aside: + toc: true +show_edit_on_github: false +show_date: false +--- + +## Implementing Branch Structures + +In our previous lesson on [Basic Structures]({{ '/notes/basic-structures' | relative_url}}) we discuss a few flow structures that we will find again and again a computer code. The first one and the most fundamental one is the sequential structure. Everything else is based on this. The second one is the *branch* structure which we will discuss more deeply in this lesson. + +We showed an example of when this branch structure may at play in a real application. For example, in our cycling application, we may want to check if the user hit the set target or not. If the user hits the target, we want to display some congratulatory message. In that lesson we showed how we plan to solve the problem using a flowchart. In this lesson, we will show how we can **I**mplement our design of algorithm using Python. Python statement that allows us to do is is the **if statement**. + +### When Things are Right + +Let's start with a simple decision making. Using our cycling app example, let's say if the user hits the set target, we will display `"You hit your cadence target for the week! Well done."`. The flowchart is shown below. + + + +We can implement the above using the following code. + +```python +if cadence >= target: + print("You hit your cadence target for the week! Well done.") +``` + +Notice the syntax above. The general syntax is as follows. + +```python +if condition: + # code block A + # executed when condition is true +``` + +The condition uses the relational operator `>=` which evaluates to boolean values either `True` or `False`. We have just learnt about boolean values in the previous lesson. If the evaluated value is `True`, the lines of code indented underneath will be executed. We will call this block of code as code block A for now. Basically, code block A will be executed if the condition is true. In our example above, we only have one line of code in block A which is the `print()` statement saying the user hits the target. + +One important thing to note is that the lines of code **must be indented**. This is how Python recognize that those lines of code is part of code block A to be executed. + +What if you have some codes not indented as shown below. + + + +Run the above code till the end and see the output. As you can see that the **unindented** code is no longer part of code block A. In fact, Python consider that as the **next statement after** the if-statement. This means that it will always be executed regardless if the condition is true or false because it is simply the next statement following the sequential nature of the program flow. + + + + +This also means that it will be executed when the condition is false. See and run the following code. + + + +In the above code, we change the user's cadence to be lower than the target. Since the condition is false, the print message in code block A is not executed. However, the print statement that is unindented after the if-statement is still executed as seen in the output. + +So for now, we are able to display some message when the user hits the target, but we don't actually display any other message if the user does not hit the target. What can we do about this? + +### When It's True and When It's False + +The previous code only display message if the user hits the target. Let's say, if the user does not hit the set target, we want to display `"You missed your target but don't give up and try again."`. In this case, we can make use of the **if-else** statement as shown below. + +```python +if cadence >= target: + print("You hit your cadence target for the week! Well done.") +else: + print("You missed your target but don't give up and try again.") +``` + +The flowchart is shown here. + + + + +The syntax in general is as follows. + +```python +if condition: + # code block A + # will be executed when condition is true +else: + # code block B + # will be executed when condition is false +``` + +Let's try this in Python Tutor. The first code is when the condition is true since we set the cadence to be higher than the target (65 > 60). Check the output of the following. + + + +The second code is when the condition is false. We set the cadence to be lower (55 < 65). + + + +Again, any unindented code is considered as the **next statement** after the if-statement and will be executed regardless if the condition is true or false. See the flowchart below. + + + +### When There are More Than Two + +So far, we have only considered the case when the condition is true and when the condition is false. But what if we have more than one condition and we want to do several things. An example would be in our cycling app when we want to categorize the user's cadence. + +| cadence | category | +|------------|-----------| +| <70 | low | +| 70 to <90 | moderate | +| 90 to <100 | high | +| 100 to 120 | very high | + +We can implement the above table in Python as follows. + +```python +if 100 <= cadence and cadence <= 120: + category = 'very high' +elif 90 <= cadence and cadence < 100: + category = 'high' +elif 70 <= cadence and cadence < 90: + category = 'moderate' +else: + category = 'low' +``` + +The general syntax for **if-elif-else** statement is as follows. + +```python +if condition_1: + # code block A + # will be executed when condition_1 is true +elif condition_2: + # code block B + # will be executed when condition_1 is false + # and condition_2 is true +elif condition_3: + # code block C + # will be executed when condition_1 and 2 are false + # and condition_3 is true +. +. +. +else: + # code block D + # will be executed when all the previous conditions are false +``` + +One important note is that the **conditions are checked sequentially from the top to the bottom**. This is described in the following flowchart. + + + +It is imperative for us to compare the above structure with the following structure. + +```python +if condition_1: + # do action 1 +if condition_2: + # do action 2 +if condition_3: + # do action 3 +else: + # do action 4 +``` + +First, notice the difference in the keyword that we use. In the previous code, we use **if** and **elif**. On the other hand, this code uses **if** and **if** for the various conditions. The flowchart below shows what it looks like for the if-if codes. + + + +Basically, the next if-statement for condition_2 and condition_3 are just the next statement after the previous if-statement. This means that each of this conditions will be checked regardless whether condition_1 is true or false. On the other hand, the if-elif statement does something different. If the earlier condition is true, it will execute the block and immediately go to the end and execute the next statement. See flowchart for the if-elif and notice the flow when condition_1 is true. It does not go and check condition_2 at all. + + + +We can show this difference using the following code examples. Let's start with the if-elif statements. + + + +In the above code, we set the user cadence to be 105. This falls under the 'very high' category. If you run the code to the last step, you will see the output `very high`. Now, what happens if we use if-if structure? + + + +When you run the above code till the end, you see that it gives a wrong output, i.e. `low`. The reason is the following. +- at step 3 of 8, the computer checks if the cadence is between 100 and 120. Since the user's cadence is 105, it will execute line 5 and set `category = 'very high'` (step 4 of 8). +- However, since, we do not use `elif` keyword, the program counter does not go to `print(category)`. On the other hand, it simply goes to the next statement which is another if-statement. +- In this if-statement, the computer again checks if the user's cadence is between 90 and 100. Since it is false, it will not set category to high. Line 7 will not be executed. What happens is that the computer goes to the next statement at line 8. +- At line 8 (step 6 of 8), the computer again checks if the user's cadence is between 70 and 90. Since it is false, it will go to the `else` block and set `category = 'low'`. This is the reason why at the end `category` is set to `low`. + +Therefore, we **must always use** if-elif statement when it is the same comparison over several conditions. Using several if-statements for these cases are common mistakes that novice programmers may make. + +## Nested If-Else + +In our previous discussion on basic structures, we noted that you can actually have a nested structure. The most common one is to have some sequential structure in one of the blocks inside either branch or iterative structure. This means the "code block A" when the condition is True may contain more than one statement but a few and they are all executed sequentially. However, such nested structure is not limited to only sequential structure. We can have a branch structure inside another branch structure. + +Recall our example of our cycling application. In our previous lessons, we drafted the following pseudocode. + +``` +1. if the *average_cadence* is greater than or equal to *target_cadence* + 1.1 Display "Good job, you hit your target for the past week cycling.". +2. Otherwise, + 2.1 calculate difference betwen *average_cadence* and *target_cadence*. + 2.2 Determine state depending on the difference + 2.2.1 if the absolute difference is less than 10 + 2.2.1.1 Display, "You almost hit your target, try harder in the coming weeks." + 2.2.2 Otherwise, + 2.2.2.1 Call *modify_target_cadence* function. +``` + +Notice in the above pseudocode that we have two branch structure. The first branch structure is based on whether the `average_cadence` is greater than or equla to the `target_cadence`. When the condition is `False`, step 2.1 calculates the difference between the two. Furthermore, step 2.2 actually contains another branch structure that is based on the difference. We can draw the flowchart as below. + + + +Note that the nested branch structure can be either in the true block or in the false block of codes. Where they are located really depends on the logic and the problem we are trying to solve. + +The way we will introduce another branch structure is basically just to include another if-statement at the **right indentation** level. Recall that Python identifies what is block of codes to be executed when it is `True` or `False` depends on the indentation of the code. For example, the above pseudocode or flowchart can be implemented in Python as follows. + +```python +if average_cadence >= target_cadence: + print("Good job, you hit your target for the past week cycling.") +else: + difference = abs(average_cadence - target_cadence) + if difference < 10: + print("You almost hit your target, try harder in the coming weeks.") + else: + target_cadence = modify_target_cadence() +``` + +It is important to take note of the indentation level when reading Python codes. In the above code we have one statement in the True block if `average_cadence >= target_cadence` which is the `print()` statement. On the other hand, there are two statements in the False block when the condition is `False`. In that False block of codes, we have the assignment to calculate the `difference` and another if-statement to check how big the difference is. This is the other branch structure that is nested in the earlier branch structure. If the difference is less than 10, it will call the `print()` function. On the other hand, if the difference is not less than 10, it will call `modify_target_cadence()` function. + +Now you know how to have a nested if-else. But what is more important is to come up with the pseudocode and the algorithm as well as identifying the basic structures. + +In some cases, you can actually reduce the level of the nested if-statements. For example, in our case, above, you can rewrite the nested if-else using if-elif-else statements. + +```python +if average_cadence >= target_cadence: + print("Good job, you hit your target for the past week cycling.") +elif abs(average_cadence - target_cadence) < 10: + print("You almost hit your target, try harder in the coming weeks.") +else: + target_cadence = modify_target_cadence() +``` + +In this case, the elif condition is checked only if `average_cadence` is not greater than or equal to `target_cadence`. This results in the same logic. Which way to write is better? Some times having a nested if-else statement is done for simplicity and clarity of the logic. However, when the nested levels become too deep, it will become harder to read. We should strive to avoid nested code without sacrificing the readability of the code. + +## Identifying Branch Structure in a Problem + +How can we identify a branch structure from our **D**esign of algorithm? The answer is simple. Whenever we encounter a step in the algorithm that requires us to take **different actions** depending on some **conditions** we have a branch structure. This requires us to make a **decision** on which course of action to take. This is why the flowchart symbol for a branch structure contains the "Decision" symbol. + + + +In order for us to easily spot the branch structure it is recommended that we rewrite our **D**esign of algorithm with certain keywords such as the following: +- decide ..., determine ... +- if ... +- otherwise ... + +This is an example of our pseudocode for our average cadence. + +``` +1. if the *average_cadence* is greater than or equal to *target_cadence* + 1.1 Display "Good job, you hit your target for the past week cycling.". +2. Otherwise, + 2.1 calculate difference betwen *average_cadence* and *target_cadence*. + 2.2 Determine state depending on the difference + 2.2.1 if the absolute difference is less than 10 + 2.2.1.1 Display, "You almost hit your target, try harder in the coming weeks." + 2.2.2 Otherwise, + 2.2.2.1 Call *modify_target_cadence* function. +``` + +Notice in the above pseudocode we use the keywords: if ..., otherwise ..., determine ..., etc. This helps us to translate the above pseudocode using the if-statement in Python easily. + + +## Abstracting Selection Process as a Function + +In some cases, we want to abstract our selection process using a function. Recall that we can use function as an abstraction of some computation such as calculating speed, calculating cadence, etc. Similarly, we can create functions that returns a Boolean data for our selection process. For example, we can create the following functions for our example on average cadence of the week. +- `user_hits_target(user_value, target)` which checks if the user hits the target or not. +- `has_small_difference(user_value, target)` which checks if the difference is small or not. We can use this to decide whether to modify the target or just display some encouraging message as in step 2.2 in the pseudocode above. + +In the two functions above, we need to return a Boolean data as the return value. Let's see how we can define those two functions. + +```python +def user_hits_target(user_value, target): + return user_value >= target +``` + +We can also write that function using type annotation as follows. + +```python + +def user_hits_target(user_value: float, target: float) -> bool: + return user_value >= target +``` + +Similarly, we can have the following function to check if the difference is large or not. + +```python +def has_small_difference(user_value, target): + return abs(user_value - target) < 10 +``` +Its type annotated function can be written as follows. + +```python +def has_small_difference(user_value: float, target: float) -> bool: + return abs(user_value - target) < 10 +``` + +With these two functions, we can re-write our if-else statements by calling these two functions. + +```python +if user_hits_target(average_cadence, target_cadence): + print("Good job, you hit your target for the past week cycling.") +elif has_small_difference(average_cadence, target_cadence): + print("You almost hit your target, try harder in the coming weeks.") +else: + target_cadence = modify_target_cadence() +``` + +You may wonder what's the advantage of abstracting the condition as a function. One advantage is that the if-statement is more readable as it is written in a higher abstraction. It is easier to read a condition that says "if it has small difference" as compared to read something with some mathematical operations in it. The second advantage is that if our definition of small difference changes, we do not need to change our if-statement code. Consider for example, if different user may have different motivation level and for some, a difference of 10 is significant while for others is not significant. What if we want to change when we should modify the target? Let's say if the difference is smaller than 20 instead. If we abstract our conditions as a function, we only need to change our function `has_small_difference()`. This is more significant if there are more than one places where we want to check if the difference is large or not. + +## Decomposing Problems Containing Branch Structures + +Another advantage of abstracting those boolean conditions into a function is that it fits nicely to how we think from big problems and decomposing it to smaller problems. For example, in our case above, we can start writing our pseudocode in this manner. + +``` +1. if the user hits the target + 1.1 Display "Good job, you hit your target for the past week cycling.". +2. Otherwise, + 2.1 calculate difference betwen *average_cadence* and *target_cadence*. + 2.2 Determine state depending on whether it has a small difference between the user and the target +``` + +We can then expand 2.2 into the following. + +``` + 2.2.1 if it has a small difference + 2.2.1.1 Display, "You almost hit your target, try harder in the coming weeks." + 2.2.2 Otherwise, + 2.2.2.1 Call *modify_target_cadence* function. +``` + +As you can see that we try to think in terms of the big picture with big steps that may contain smaller steps. Step 2.2 for example may be broken down into 2.2.1 and 2.2.2. Moreover, we can decompose the step `if the user hits the target` into the following: + +``` +if the average_cadence is greater than or equal to target_cadence +``` + +In this case, the second statement is more concrete while the original one is more abstract. + +Similarly, we can drill down step 2.2.1 `if it has small difference` into the following. + +``` +if the difference is less than 10 +``` + +We should not be afraid of revising our **D**esign of algorithm at various level of abstraction. In fact, that is one of important lesson in computational thinking. To be able to think through some abstraction and at different level of abstraction is one of the key element of computational thinking. \ No newline at end of file diff --git a/_Notes/BubbleSort_InsertionSort.md b/_Notes/BubbleSort_InsertionSort.md deleted file mode 100644 index 19d67f7..0000000 --- a/_Notes/BubbleSort_InsertionSort.md +++ /dev/null @@ -1,484 +0,0 @@ ---- -title: Bubble Sort and Insertion Sort -permalink: /notes/bubble_insertion_sort -key: notes-sort-intro -layout: article -nav_key: Notes -sidebar: - nav: Notes -license: false -aside: - toc: true -show_edit_on_github: false -show_date: false ---- -# Sorting Algorithm - -The best way to practice your programming skills is by writing actual code. One of the common computation is to sort some items in some way. For example, sorting a number from smallest to biggest or names from A to Z. In this notebook, we will describe some sorting algorithms which you can implement in Python. - -## Bubble Sort - -Bubble sort is one of the simplest sorting algorithms. We will be following the PCDIT framework (**P**roblem statement, Test **C**ases, **D**esign of Algorithm, **I**mplementation, and **T**esting) in describing the steps of these algorithms. - - -### Problem Statement - -The problem is specified as follows. Given a sequence of numbers, write some steps to sort the sequence in some order. Usually, we will sort the sequence from the smallest to the largest. - -### Test Case - -For example, given the following input: - -```python -# Python Code -numbers = [16, 14, 10, 8, 7, 8, 3, 2, 4, 1] -``` - -We want to write some steps that sort the numbers, such that the output will be: - -```python -[1, 2, 3, 4, 7, 8, 8, 10, 14, 16] -``` - -We can intuitively try to sort the numbers by comparing two numbers (a pair) at a time. If the order is incorrect, we will swap the two numbers. Let's illustrate the steps! - -* We start from the input: - - [**16, 14**, 10, 8, 7, 8, 3, 2, 4, 1] - -* We compare the first two numbers (16, 14), i.e. **positions 0 and 1** in the list. Since 16 is bigger than 14, we will swap them. - - [**14, 16**, 10, 8, 7, 8, 3, 2, 4, 1] - -* Now we move to the next pair (16, 10), i.e. **positions 1 and 2** in the list. Since 16 is bigger than 10, we will swap them again. - - [14, **16, 10**, 8, 7, 8, 3, 2, 4, 1] - - [14, **10, 16**, 8, 7, 8, 3, 2, 4, 1] - -* And next pair (16, 8), i.e. **positions 2 and 3**. Since 16 is bigger than 8, we will swap. - - [14, 10, **16, 8**, 7, 8, 3, 2, 4, 1] - - [14, 10, **8, 16**, 7, 8, 3, 2, 4, 1] - -* We continue until 16 reaches the last position. - - [14, 10, 8, **16, 7**, 8, 3, 2, 4, 1] - - [14, 10, 8, **7, 16**, 8, 3, 2, 4, 1] - - -- - - [14, 10, 8, 7, **16, 8**, 3, 2, 4, 1] - - [14, 10, 8, 7, **8, 16**, 3, 2, 4, 1] - - -- - - [14, 10, 8, 7, 8, **16, 3**, 2, 4, 1] - - [14, 10, 8, 7, 8, **3, 16**, 2, 4, 1] - - -- - - [14, 10, 8, 7, 8, 3, **16, 2**, 4, 1] - - [14, 10, 8, 7, 8, 3, **2, 16**, 4, 1] - - -- - - [14, 10, 8, 7, 8, 3, 2, **16, 4**, 1] - - [14, 10, 8, 7, 8, 3, 2, **4, 16**, 1] - - -- - - [14, 10, 8, 7, 8, 3, 2, 4, **16, 1**] - - [14, 10, 8, 7, 8, 3, 2, 4, **1, 16**] - -So now the largest number has occupied its place in the last position. But the other numbers are still not in the right order. Therefore, we have to repeat the steps starting from the beginning again. We start from the pair (14, 10). This will repeat the **pair-wise** comparison and move 14 to the second last position. Similarly, we can see how 10 and 8 will reach their final position. Here, we no longer show the pair-wise comparison and swapping for brevity. -* [10, 8, 7, 8, 3, 2, 4, 1, **14, 16**] -* [8, 7, 8, 3, 2, 4, 1, **10, 14, 16**] -* [7, 8, 3, 2, 4, 1, **8, 10, 14, 16**] - -At this point, we will start comparing the pair (7, 8). **But since 7 is not greater than 8, there is no swap**. - -* [**7, 8**, 3, 2, 4, 1, **8, 10, 14, 16**] - -Afterward, we will compare the pair (8, 3). - -* [7, **8, 3**, 2, 4, 1, **8, 10, 14, 16**] - -Since 8 is greater than 3, **there will be a swap**, and these **pair-wise swaps** will continue to push 8 to its final position. -* [7, 3, 2, 4, 1, **8, 8, 10, 14, 16**] - -Now we begin again with the pair (7, 3) and push 7 to its final position. The detailed pair-wise swapping is not shown below, but the final arrangement at the end of this iteration will be as the one below. - -* [3, 2, 4, 1, **7, 8, 8, 10, 14, 16**] - -At this point, we will start a new iteration by comparing the pair (3, 2). Since 3 is greater than 2, there will be a swap. The next comparison will fall on the pair (3, 4), i.e. [2, **3, 4**, 1, 7, 8, 8, 10, 14, 16]. But since 3 is less than 4, there is no swap happing. And the rest of the comparison will push 4 to its final position. -* [2, 3, 1, **4, 7, 8, 8, 10, 14, 16**] - -A similar situation occurs. There is no swap between (2, 3), but then it will swap (3, 1). - -* [2, 1, **3, 4, 7, 8, 8, 10, 14, 16**] - -In the last iteration, it wil swap (2, 1). - -* [**1, 2, 3, 4, 7, 8, 8, 10, 14, 16**] - -Here's an animated example from Wikipedia,sorting a different sequence of numbers. - -![Animation for Bubble Sort](https://upload.wikimedia.org/wikipedia/commons/c/c8/Bubble-sort-example-300px.gif) - -### Design of Algorithm - -After we work on the test cases, we can now write down the steps in pseudocode. Several things to note from the above test cases: - -* There are two kind of iterations: - 1. the *inner* iteration is when we compare the pairs (a, b) and do a swap if $a>b$, - 1. the *outer* iteration is when repeat the inner iteration pass starting from the first pair again. -* The number of *inner* iteration is the $n-1$, where $n$ is the number of elements in the list. This is because the inner iteration compares a pair. So if there is $n$ elements, there will be $n-1$ pairs to compare. -* The number of *outer* iteration is also $n-1$. You can refer back to the case above that there were 9 *outer* iterations for the 10 elements. - -Let's write down what we did in the above case. Note that in this algorithm we chose not to return the sorted list as a new object, but rather _sort the list in place_. This means that the input list is modified and the sorted list exists in the object pointed by the input list. The advantage of this is that the list need not be duplicated and the memory is saved as we deal only with one list. - -``` -Bubble Sort -Version: 1 -Input: array -Output: None, sort in place -Steps: -1. n = length of array -2. For outer_index from 1 to n-1, do: - 2.1 For inner_index from 1 to n-1, do: - 2.1.1 first_number = array[inner_index - 1] - 2.1.2 second_number = array[inner_index] - 2.1.3 if first_number > second_number, do: - 2.1.3.1 swap(first_number, second_number) - -``` - -## Optimised Bubble Sort - -We can optimized bubble sort algorithm by noting the following: - -* If the sequence is already in order, we can reduce the next *outer* iteration. For example, if the input is - - [16, 1, 2, 3, 4, 5] - - The first iteration will push 16 to the last position. - - [1, 2, 3, 4, 5, **16**] - - In the second iteration, no swap is made since all the numbers are already in the correct order. However, with the above algorithm, the outer iteration will still repeat for $n-1$ times. We can conclude therefore that if no swap is made in one pass of *outer* iteration, the sequence is already in order, and we can stop the *outer* iteration. We can then modify the pseudocode as follows. - - ``` - Bubble Sort - Version: 2 - Input: array - Output: None, sort in place - Steps: - 1. n = length of array - 2. swapped = True - 3. As long as swapped is True, do: - 3.1 swapped = False - 3.2 For inner_index from 1 to n-1, do: - 3.2.1 first_number = array[inner_index - 1] - 3.2.2 second_number = array[inner_index] - 3.2.3 if first_number > second_number, do: - 3.2.3.1 swap(first_number, second_number) - 3.2.3.2 swapped = True - ``` - -Let's compare this with version 1. -* Notice that the number of *outer* iteration is much less in version two as compared to version one. -* The n-th pass of the *outer* iteration will place the n-th largest number to its final position. For example, in the given input below, - - [16, 14, 10, 8, 7, 8, 3, 2, 4, 1] - - In the 1-st *outer* pass, the 1-st largest number (i.e. 16) will be placed to its final position. - - [14, 10, 8, 7, 8, 3, 2, 4, 1, **16**] - - This means that we can reduce the number of *inner* iteration. Instead of comparing $n-1$ pairs for each *inner* iteration, we can reduce the number of *inner* iteration after each pass of *outer* iteration is done. For example, in the above example, we have 10 elements. In the first *outer* iteration, we have 9 comparisons in the *inner* iteration. In the next *outer* iteration, we can simply do 8 comparisons instead of 9. In the third *outer* iteration, we can do just 7 comparisons instead of 9, and so on. - - We can re-write the pseudocode as follows. - - ``` - Bubble Sort - Version: 3 - Input: array - Output: None, sort in place - Steps: - 1. n = length of array - 2. swapped = True - 3. As long as swapped is True, do: - 3.1 swapped = False - 3.2 For inner_index from 1 to n-1, do: - 3.2.1 first_number = array[inner_index - 1] - 3.2.2 second_number = array[inner_index] - 3.2.3 if first_number > second_number, do: - 3.2.3.1 swap(first_number, second_number) - 3.2.3.2 swapped = True - 3.3 n = n -1 - ``` - - The additional step is 3.3 which is to reduce the number of $n$. With this, Step 3.2 will decrease by one in the next *outer* iteration. - -* It can happen that more than one elements are place in their final positions in one *outer* iteration pass. This means that we don't have to decrease the number of *inner* iteration by 1 on each step of *outer* iteration. We can record down, at which position was the last swap, and on the next *outer* iteration, we can do comparison up to that last position. To illustrate this, consider the following input. - - [1000, 1, 4, 7, 3, 10, 100] - - In the first *outer* pass, we push 1000 to its final position. This means we have $n-1$ comparisons and swaps. - - [1, 4, 7, 3, 10, 100, 1000] - - In the second *outer* pass, we first compare the pair (1, 4), but no swap happens. Similarly with (4, 7). Now, when comparing the pair (7, 3), we do a swap to result in. - - [1, 4, **3, 7**, 10, 100, 1000] - - When we have a swap, we will record down our position. In this case, it is at the position of the **4th element in the list, or index 3** (if our index starts from 0). Subsequently, we compare (7, 10), but no swap happens. Similarly with (10, 100). We stop at this point because the second iteration only compares up to the second last element in the above algorithm. - - Now, if we follow the previous algorithm, the next *outer* pass will compare up to the third last element, which is 10. However, we note that the last five elements are already ordered. We know this because in the last pass, the last swap happens at (7, 3) to (3, 7) pair. In this case, we just need to run our *inner* iteration up to this position, i.e. **4th position, or index 3**. - - We can modify our pseudocode as follows. - - ``` - Bubble Sort - Version: 4 - Input: array - Output: None, sort in place - Steps: - 1. n = length of array - 2. swapped = True - 3. As long as swapped is True, do: - 3.1 swapped = False - 3.2 new_n = 0 - 3.3 For inner_index from 1 to n-1, do: - 3.3.1 first_number = array[inner_index - 1] - 3.3.2 second_number = array[inner_index] - 3.3.3 if first_number > second_number, do: - 3.3.3.1 swap(first_number, second_number) - 3.3.3.2 swapped = True - 3.3.3.3 new_n = inner_index - 3.4 n = new_n - ``` - - In the above pseudocode, we set record down the position of the element on the last swap (step 3.3.3.3), and we assign this as the new ending position for the next *outer* pass (step 3.4). - -## Insertion Sort - -Insertion sort is another algorithm that solves the same problem. Let's start by looking at the same test case. - -### Test Case - -Given the following input: - -```python -# Python Code -numbers = [16, 14, 10, 8, 7, 8, 3, 2, 4, 1] -``` - -We want to write some steps that sort the numbers such that the output will be: - -```python -[1, 2, 3, 4, 7, 8, 8, 10, 14, 16] -``` - -* We start from the *second* element in the list, i.e. 14. - - [16, **14**, 10, 8, 7, 8, 3, 2, 4, 1] - -* We then compare that number with the one on the left. If it is smaller, then we will swap. Since $14<16$, we do a swap. - - [**14, 16**, 10, 8, 7, 8, 3, 2, 4, 1] - -* Since 14 has reached its place, we now move to the *third* element in the list, i.e. 10. Since $10 < 16$, we swap (16, 10). - - [14, **16, 10**, 8, 7, 8, 3, 2, 4, 1] - - [14, **10, 16**, 8, 7, 8, 3, 2, 4, 1] - - Now we continue comparing 10 with the one on its left, i.e. 14. Since $10<14$, we swap (14, 10). - - [**14, 10**, 16, 8, 7, 8, 3, 2, 4, 1] - - [**10, 14**, 16, 8, 7, 8, 3, 2, 4, 1] - - Now 10 has reached its position. - -* We now move to the *fourth* element, i.e. 8, and compare it with the number on its left. Since $8 < 16$, we swap (16, 8). We then continue swapping until 8 reaches its position. - - [10, 14, **16, 8**, 7, 8, 3, 2, 4, 1] - - [10, 14, **8, 16**, 7, 8, 3, 2, 4, 1] - - ---- - - [10, **14, 8**, 16, 7, 8, 3, 2, 4, 1] - - [10, **8, 14**, 16, 7, 8, 3, 2, 4, 1] - - ---- - - [**10, 8**, 14, 16, 7, 8, 3, 2, 4, 1] - - [**8, 10**, 14, 16, 7, 8, 3, 2, 4, 1] - -* We now move to the *fifth* element, i.e. 7. We then have the same swapping all the way until 7 reaches its place. - - [8, 10, 14, **16, 7**, 8, 3, 2, 4, 1] - - [8, 10, 14, **7, 16**, 8, 3, 2, 4, 1] - - ---- - - [8, 10, **14, 7**, 16, 8, 3, 2, 4, 1] - - [8, 10, **7, 14**, 16, 8, 3, 2, 4, 1] - - ---- - - [8, **10, 7**, 14, 16, 8, 3, 2, 4, 1] - - [8, **7, 10**, 14, 16, 8, 3, 2, 4, 1] - - ---- - - [**8, 7**, 10, 14, 16, 8, 3, 2, 4, 1] - - [**7, 8**, 10, 14, 16, 8, 3, 2, 4, 1] - -* We now move to the *sixth* element, i.e. 8. We will continue swapping until 8 encounters another 8 in the 2nd element. At this point, the swapping will stop. - - [7, 8, 10, 14, **16, 8**, 3, 2, 4, 1] - - [7, 8, 10, 14, **8, 16**, 3, 2, 4, 1] - - ---- - - [7, 8, 10, **14, 8**, 16, 3, 2, 4, 1] - - [7, 8, 10, **8, 14**, 16, 3, 2, 4, 1] - - ---- - - [7, 8, **10, 8**, 14, 16, 3, 2, 4, 1] - - [7, 8, **8, 10**, 14, 16, 3, 2, 4, 1] - - ---- - - [7, **8, 8**, 10, 14, 16, 3, 2, 4, 1] - - no swapping occurs - -* We can now move to the *seventh* element, i.e. 3. We will not show the swapping steps, and only show the final position of the sevent element. - - [**3**, 7, 8, 8, 10, 14, 16, 2, 4, 1] - -* We do the same with the *eight* element, i.e. 2. - - [**2**, 3, 7, 8, 8, 10, 14, 16, 4, 1] - -* Similary with the *nineth* element, i.e. 4. However, this element will stop swapping when it sees the number lower than itself, so it will stop when it sees 3. - - [2, 3, **4**, 7, 8, 8, 10, 14, 16, 1] - -* Lastly, the *tenth* element, i.e. 1, will move all the way to the first position. - - [**1**, 2, 3, 4, 7, 8, 8, 10, 14, 16] - -### Design of Algorithm - -Looking at the above case, we can try to write down our algorithm in pseudocode. Several things to note: - -* There are two iterations in the steps above: - 1. *outer* iteration is moving from the *second* element to the last element in the list. What the outer iteration does is to place that n-th element into its position. - 1. *inner* iteration is swapping the n-th element until either: - * it reaches the most left position, or - * the number on its left is smaller -* The number of *outer* iteration is fixed, which is $n-1$. This is because it starts from the second element to the last element. So if there are $n$ elements, it will repeat itself $n-1$ times. -* The *outer* iteration starts from the second element, which is index 1. -* The number of *inner* iteration is not fixed since it depends on the two cases stated above. The maximum number of iteration is when the number reaches the most left position. In this case for element at position $i$, it will repeat $i$ times. If it sees a number that is smaller than itself, the number of iteration for the element at position $i$ will be less than $i$. - -Let's write it down. - -``` -Insertion Sort -Version: 1 -Input: array -Output: None, sort in place -Steps: -1. n = length of array -2. For outer_index in Range(from 1 to n-1), do: - 2.1 inner_index = outer_index # start with the i-th element - 2.2 As long as (inner_index > 0) AND (array[inner_index] < array[inner_index - 1]), do: - 2.2.1 swap(array[inner_index - 1], array[inner_index]) - 2.2.2 inner_index = inner_index - 1 # move to the left -``` -### Optimised Insertion Sort - -We can improve the algorithm slightly by reducing the number of assignment in the inner loop. This means that instead of swapping and assigning elements in the *inner* iteration, we only assign the element once it finds its final position. To do this, we store the element we are going to move into a temporary variable. - -* Let's illustrate this when the *outer* iteration is moving the *sixth* element, i.e. 8. - - [7, 8, 10, 14, 16, **8**, 3, 2, 4, 1] - -* Instead of swapping (16, 8) pair, we store 8 into a temporary variable. Then we compare the temporary variable with 16. Since $8 < 16$, we simply shift 16 to the right. We indicate the position to be replaced with an underscore below. Since no swap is being done, the old value remains after the shift. - - [7, 8, 10, 14, **16**, `_8_`, 3, 2, 4, 1] , temporary = 8 - - [7, 8, 10, 14, `_16_`, **16**, 3, 2, 4, 1] , temporary = 8 - -* We then now compare, the temporary variable with 14. Since $ 8 < 14$, we shift 14 to the right. - - [7, 8, 10, **14**, `_16_`, 16, 3, 2, 4, 1] , temporary = 8 - - [7, 8, 10, `_14_`, **14**, 16, 3, 2, 4, 1] , temporary = 8 - -* We do the same with 10. - - [7, 8, **10**, `_14_`, 14, 16, 3, 2, 4, 1] , temporary = 8 - - [7, 8, `_10_`, **10**, 14, 16, 3, 2, 4, 1] , temporary = 8 - -* Now, we compare the temporary variable with 8. But since 8 is not less than the value in the temporary variable, we do not swap, and store the temporary value to element on the right of it. We can then stop the *inner* iteration, and move to the next pass of *outer* iteration. - - [7, **8**, `_10_`, 10, 14, 16, 3, 2, 4, 1] , temporary = 8 - - [7, 8, **8**, 10, 14, 16, 3, 2, 4, 1] , temporary = 8 - -This is an animation example for a different sequence of number from Wikipedia. - -![Animation for Insertion Sort](https://upload.wikimedia.org/wikipedia/commons/9/9c/Insertion-sort-example.gif) - -We can then modify the pseudocode as follows. - -``` -Insertion Sort -Version: 2 -Input: array -Output: None, sort in place -Steps: -1. n = length of array -2. For outer_index in Range(from 1 to n-1), do: - 2.1 inner_index = outer_index # start with the i-th element - 2.2 temporary = array[inner_index] - 2.3 As long as (inner_index > 0) AND (temporary < array[inner_index - 1]), do: - 2.3.1 array[inner_index] = array[inner_index - 1]) # shift to the right - 2.3.2 inner_index = inner_index - 1 # move to the left - 2.4 array[inner_index] = temporary # save temporary to its final position -``` - -Note: - -* The additional step is on 2.2 where we store the $i$-th element to a temporary variable. -* Step 2.3 condition is modified to compare the temporary variable with the element pointed by `inner_index - 1`. -* Step 2.3.1 now only have one assignment instead of two. -* We only assign the temporary to the final position in step 2.4. -* Note that the position stored in 2.4 is `inner_index` instead of `inner_index - 1`. The reason is that in the last iteration, we have executed 2.3.2 which reduces the index by one. -* Assignment from temporary variable to the array only happens at the end of *inner* iteration. diff --git a/_Notes/Calling_Function.md b/_Notes/Calling_Function.md new file mode 100644 index 0000000..ffaaa4c --- /dev/null +++ b/_Notes/Calling_Function.md @@ -0,0 +1,185 @@ +--- +title: Calling a Function +permalink: /notes/calling-function +key: notes-calling-function +layout: article +nav_key: Notes +sidebar: + nav: Notes +license: false +aside: + toc: true +show_edit_on_github: false +show_date: false +--- + +## Functions, Our First Abstraction + +In our previous lessons, we have used a few built-in functions: +- `print()` to display data into the standard output, which in most cases is your computer screen. +- `int()` to convert a value into an `int` data type. +- `input()` to get a value entered through a standard input device, which in most cases is your keyboard. + +We call these functions as built-in functions because Python provides these functions for our use. At the same time, we want to introduce the concept of abstraction and how functions actually provides that abstraction. For example, in order to display some data into a screen requires not a single instruction in a computer. It actually requires a number of instructions that would include part of the operating system before the data is displayed into the computer screen. However, Python **abstract** all those instruction into a single function called `print()`. + +In a way, a *function* encapsulates some computation. Every computation may or may not have some input. It may or may not have some output. Similarly, every function does some computation. Some function may or may not have some input. Some functions may or may not have some output. We can draw a function like a blackbox as shown in the diagram below. + + + +In the above diagrams, we have drawn various possible combination with regards to the input and output of a function. Let's discuss some of these in the sections below. + +## How to Call a Function + +The first important thing about function is to know how to **execute** a function. We have a term for this executing a function. We call it **calling** a function or **invoking** a function. Basically, when we call a function, we execute a whole set of instructions encapsulated by that function. We do not need to know how to display a character into a computer screen, we just need to know how to *call* the `print()` function. To call a function in Python, you need to specify using the following format. + +```python +function_name(input_arguments) +``` + +We have to specify the name of the function followed by a round bracket or parenthesis. Inside the paranthesis, we need to provide the input data required by the function to perform. For example, + +```python +print("Hello World") +int("5") +input("How many steps?") +``` + +In the above, example, `print`, `int` and `input` are the function names. You can see that all three has the parenthesis following the function name. Inside these paranthesis are the input data needed by the function to perform its computation. We call this input data as **input arguments**. So `"Hello World"`, `"5"`, and `"How many steps?"` are input argument to the respective function calls above. + +Not all functions have input arguments but the above three functions are example of functions with input arguments. Similarly, not all functions have outputs. In fact, one of those three function does not have an output. Let's see which one. + +## Output of a Function Versus Side Effects + +Input arguments are data that the function **takes in** in order to do its computation. The **output** of a function is the data that the function **throws out** as a result of some computation inside the function. In our three built-in examples, only `int()` and `input()` throws some output. The `print()` function does not throw an output. + + + +You may wonder why `print()` function does not throw an output though you can see something on the computer screen. Doesn't displaying data on a screen can be considered as an output? + +Again, let's repeat our definition of an output of a function. The output of a function is the data thrown out by the function as a result of some computation inside the function. Therefore, one way to check if a function has an output or not is to check what data is coming out from that function. We can store data into some variables as we saw in our previous lessons. For example, + +```python +cadence: int = 25 +``` + +In the above line, we create variable called `cadence` which is an integer and assign a value `25`. Since the literal `25` is interpreted by Python as an `int` data, cadence is binded to an `int` data `25`. We can consider this assignment as *storing* the value `25` into the variable `cadence`. + + + +More accurately, we are binding a value with a name. The value is `25` and the name is `cadence`. The assignment operator creates the *binding*. This binding can be represented as a table that associates a name and a value. + +| Name | Value | +|---------|-------| +| cadence | 25 | +| | | + +Now, we can see if a function throws any output data by storing that output data into a variable. We will use the same **assignment** operator to assign the output of the function to the variable and then we will display this variable to the screen. + +```python +out_print = print("Hello World") +print(out_print) + +out_int = int("5") +print(out_int) + +out_input = input("What's your steps?") +print(out_input) +``` + + + +Try the above code and enter `25` as your number of steps. You will get something like the following output. + +``` +Hello World +None +5 +What's your steps? 25 +25 +``` + +The first line displays `Hello World` and this is due to the print function where the input argument is that string data to be displayed. However, the next line contains `None`. This line is due to the `print(out_print)` statement. As we can see that the `out_print` variable stores the output of the `print()` function in the first line. The data is displayed in the second line of the standard output as `None`. The reason of this is that `print()` function does not throw any output data. Python, by default, returns a `None` object when it does not have any return output data. + +The third line of the output, however, displays `5`. This is the result of `print(out_int)`. So we know that `int()` function throws an output data, i.e. `5`. The second last line, we see the prompt being displayed into the standard output, `What's your step?`. In the last line, we see the output data thrown out by the `input()` function, i.e. `25` (assuming you enter 25 when prompted "What's your steps?"). + +So we see that both `int()` and `input()` throws some output data but `print()` does not. This *throwing out* output is more commonly called **returning an output** in programming languages and you will see later on that Python has a specific keyword if you want to return an output from your user-defined function. We will learn how to create our own user-defined function soon. + +But we may ask, so what is the thing that we see on the screen when we call `print()` function? The answer is it is a **side effect**. A function is said to have a side effect if it modifies some state outside of its local environment. This results in some observable effect other than its primary effect of returning a value to the invoker of the function. In our case, displaying a text into a screen is this observable effect. The cause of this observable effect of something appear on the screen is that the `print()` function modifies something outside of its function **not by returning a value to the program that call the print function**. We have seen that the function `print()` does not return any data to `out_print`. Because `print()` modifies something outside of its function not through its **return value**, we say that the `print()` function has a side effect. + +Now, we have differentiate an output of a function and a side effect. When we call a function, Python evaluates its **return value** as its output. For example, + +```python +out_int = int("5") +``` + +Python interpreter evaluates the return value of invoking `int("5")`. The result of this computation is the output of the function which is an `int` type data that represent `5`. This output is then assigned to the variable. The above code is then the same as the following after executing the function `int("5")`. + +```python +out_int = 5 +``` + +It is a good practice to annotate your variable to keep track of your data type. In our previous code, we should have written the following. + +```python +out: int = int("5") +``` + +Instead of putting the "int" as part of the variable name, we can use type annotation. The good thing about type annotation is that we can use tools such as `mypy` to do a static check on our code. + + + +| Name | Value | +|------|-------| +| out | 5 | +| | | + +## Importing and Calling Math Functions in Python + +We have learnt how to call functions and supplying the input arguments as well as how to retrieve the output returned by the function. Now we can work with more functions. One of the common functions that Python provides is the math library. In order to use the math library, we need to import them into the current environment. + +```python +import math +``` + +You can find a list of math functions [in this link](https://docs.python.org/3/library/math.html). + +For example, if you want to find a square root of a number, you can call the `sqrt()` function from the `math` library. + +```python +import math +x: float = math.sqrt(4) +``` + +The value of `x` after executing the function is `2`. Notice that we are importing the `math` name into our environment. This `math` name contains a reference to the `math` module that contains a range of functions. In order to access that function inside that module, we use the **dot** operator as in `math.sqrt()`. Notice also that our `math.sqrt()` function returns a float and not an integer. Because of this, we annotate `x` as a float. + +Sometimes, we are too lazy to keep on typing `math` everytime, we want to call a function. In this case, we can rename it using the keyword `as`. + +```python +import math as m +x: float = m.sqrt(4) +``` + +If we do not prefer to type `m` at all, we can simply import the function itself instead of importing the module into our environment. In this case, we do it as shown below. + +```python +from math import sqrt +x: float = sqrt(4) +``` + +In the last example, what we imported is only the function `sqrt` which we get from the `math` module. If two packages provide a similar function name, your global frame will be populated by only the last name you define or imported. If you prefer to keep separately the two functions for different usage, it is recommended to use the second approach above by renaming the different modules and access the same function name using the dot operator. + +For example, `numpy` also provides a `sqrt` function. We can import both function as follows. + +```python +import math as m +import numpy as np +x: float = m.sqrt(4) +y: float = np.sqrt(4) +``` + +You maybe wondering what's the use of `numpy` package if `math` package already provides the same square root function. Python's `math` library deals with either `int` or `float` data type but not a collection of these data. On the other hand, `numpy` can deal better with array-like data. In later section, we will learn about some collection data like `list` which `numpy` array can handle better. For `math` library, we need to loop over those values in the collection in order to apply the function. There is another way like using the `map` function to do similar thing. For now, our objective is on how to call functions and how to import functions from another library like `math`. + + +## Summary + +In this lesson, you have learnt how to call a function. Some functions require you to supply the input arguments for the function to do its calculation. You also learn how take the return value of the function which we also call as the output of the function. We differentiate the output returned by the function and its side effect. Some functions like `print()` do not have output but creates side effect. We also learn on how you can make use of other functions created by other people such as the various mathematical function in the `math` library. We show the different ways of importing the module into the current environment so that we can call the function using the dot operator. diff --git a/_Notes/ComputationTime.md b/_Notes/ComputationTime.md deleted file mode 100644 index 03238c5..0000000 --- a/_Notes/ComputationTime.md +++ /dev/null @@ -1,687 +0,0 @@ ---- -title: Computation Time -permalink: /notes/computation_time -key: notes-computation-time -layout: article -nav_key: Notes -sidebar: - nav: Notes -license: false -aside: - toc: true -show_edit_on_github: false -show_date: false ---- - -A performance of a computer program can be measured using its computation time and its memory space that it occupies. In this section, we will focus on analysing and measuring computation time. - -## Asymptotic Notation - -Asymptotic notation is a shorthand used to give a quick measure of the behaviour of a function $f(n)$ as $n$ grows large. Here, we will introduce several notations that is common in analysing computer programs. - - -### Little Oh - -This notation is to indicate that $f$ is *asymptotically smaller* than $g$, or in symbols, - -$$f(x) = o(g(x))$$ - -This is True if and only if - -$$\lim_{x\to \infty} \frac{f(x)}{g(x)} = 0$$ - -For example, - -$$1000x^{1.9} = o(x^2)$$ - -This is because - -$$\lim_{x\to\infty} \frac{1000x^{1.9}}{x^2} = \lim_{x\to\infty} \frac{1000}{x^{0.1}} = 0$$ - -### Big Oh - -Big Oh is the most frequently used notation in computing as it is used to give an upper bound on the growth of a function. - -$$f = O(g)$$ - -if and only if, - -$$\lim_{x\to \infty}\text{sup} \frac{f(x)}{g(x)} < \infty$$ - -Note the word "sup" means superior or above as it indicates the upper bound. - -For example, - -$$x^2 + 100x+10 = O(x^2)$$ - -This is because - -$$\lim_{x\to\infty} \text{sup} \frac{x^2+100x+10}{x^2} = 1 < \infty$$ - -### Big Omega - -The previous notation "Big Oh" is used to indicate an upper bound. The notation to indicate the lower bound is Big Omega, or simply Omega. - -$$f = \Omega(g)$$ - -if and only if there exist a constant $c$ and an $x_0$ such that for all $x\ge x_0$, we have - -$$f(x) \ge c |g(x)|$$ - -In other words, this simply means that $f(x)$ is greater than or equal to $g(x)$. As you can guess, this sounds like Big-Oh in reverse. - -$$f(x) = O(g(x)) \text{ if and only if } g(x) = \Omega(f(x))$$ - -For example, - -$$x^2 = \Omega(x)$$ - -We will use the definition for Big Oh to prove this by exchanging the terms $f$ and $g$. We are going to prove that - -$$x = O(x^2)$$ - -This is true because - -$$\lim_{x\to\infty}\frac{x}{x^2} = \lim_{x\to\infty}\frac{1}{x} < \infty$$ - -Therefore, - -$$x^2 = \Omega(x)$$ - -#### Little Omega - -This notation is used to denote that one function grows stricly faster than another function. - -$$f(x) = \omega(g(x))$$ - -if and only if, - -$$\lim_{x\to\infty} \frac{g(x)}{f(x)} =0$$ - -This is like the reverse of little Oh, - -$$f(x) = \omega(g(x)) \text{ if and only if } g(x) = o(f(x))$$ - -For example, - -$$x^{1.5} = \omega(x)$$ - -This is true because - -$$\lim_{x\to\infty} \frac{x}{x^{1.5}} = \lim_{x\to\infty} x^{-0.5} = \lim_{x\to\infty} \frac{1}{\sqrt{x}} = 0$$ - - -#### Theta - -Sometimes, we want to specify both upper bound and lower bound at the same time. We use this notation, - -$$f = \Theta(g)$$ - -if and only if, - -$$f=O(g)\text{ and } g=O(f)$$ - -For example, - -$$10n^3-20n^2+1 = \Theta(n^3)$$ - -We then have to prove both conditions, i.e. $f=O(g)$ and $g=O(f)$. So we will start with the first one. - -$$10n^3-20n^2+1 = O(n^3)$$ - -This is true because - -$$\lim_{x\to\infty} \text{sup} \frac{10n^3-20n^2+1}{n^3} = 10 < \infty$$ - -Now, we prove the second condition. - -$$n^3 = O(10n^3-20n^2+1 )$$ - -This is also true because - -$$\lim_{x\to\infty} \text{sup} \frac{n^3}{10n^3-20n^2+1} = \frac{1}{10} < \infty$$ - -Therefore, - -$$10n^3-20n^2+1 = \Theta(n^3)$$ - -#### Analogies with Relation Operators - -These asymptotic notations can be understood better in relation to analogies with relational operators of numbers. - -| relational operator | asymptotic notation| -|--------------|------------| -| $f = g$ | $f = \Theta(g)$ | -| $f < g$ | $f = o(g)$ | -| $f <= g$ | $ f= O(g)$ | -| $f > g$ | $f = \omega(g)$ | -| $f >= g$ | $f = \Omega(g)$ | - -## Measuring Computation Time - -We are interested in the trend on the computation time as the number of input changes. For example, considering the sorting algorithms that we have considered thus far. How does the computation time increases as we increase the number of the input array? In analysing this, we are interested in the upper bound, and so we normally use the Big-Oh notation to indicate the upper bound of a computation time of a program. - -We can investigate this empirically by creating a list of different sizes, i.e. 10 elements, 100 elements, etc. We can then ask our sorting algorithm to sort these numbers. We can also compare the performance of the sorting algorithm when the input list is already sorted or when it is randomly shuffled. - -### Setup - -We generate the input array of integers with different number of elements from 10 up to 10,000, i.e. 10 elements, 100 elements, 1000 elements, and 10,000 elements. We run the sorting algorithms two times, one is when the input array is randomly shuffled and the second one is when the input array is already sorted from the smallest to the largest. We present the results for different algorithms in the next sections. - -### Bubble Sort - -If we run version 1 of Bubble Sort algorithm on the randomly shuffled array. The output time in seconds are shown here. - -```python -bubbletime = [5.7220458984375e-06, 2.2649765014648438e-05, - 0.0014679431915283203, 0.2126140594482422, - 25.051520347595215] -``` - -We can plot this and see the relationship between the number of elements and the computation time. To see the relationship better in this big range of number, we plot the log of $y$ and the log of $x$. - -```python -import matplotlib.pyplot as plt -import numpy as np - -nelements = [10, 100, 1000, 10000, 100000] -bubbletime = [5.7220458984375e-06, 2.2649765014648438e-05, - 0.0014679431915283203, 0.2126140594482422, - 25.051520347595215] - -plt.title("Bubble Sort on Randomly Shuffled Array") -plt.xlabel("log(number of input)") -plt.ylabel("log(computation time (s))") -plt.plot(np.log(nelements), np.log(bubbletime),'o-') -``` -![](/assets/images/week2/plot_time_bubblesort.jpeg) - -We can see that the computation time increases as the input increases. Moreover we can see that the relationship is almost a straight line when we plot the logarithmic of the y axis and the logarithmic of the x axis. In fact, the relationship is a quadratic. If we get the slope of this log plot, taking the x-axis between 6 to 10, we get: - -$$slope = \frac{(0 - (-8))}{10 - 6} = 2$$ - -which makes sense because if we have -$$y = x^2$$ - -Taking the log of both sides gives us - -$$\log(y) = 2 \log(x)$$ - - -In this equation the slope is 2 as shown on the plot. We can also check if it is quadratic by calculating the square of our input and see if it is a straight line. - -This means that the computation time of Bubble Sort algorithm is quadratic. - -$$T(n) = O(n^2)$$ - -On the other hand, this is the computation time when the input is already sorted. - -```python -bubbletimeSorted = [6.4373016357421875e-06, 1.9073486328125e-06, - 4.291534423828125e-06, 3.147125244140625e-05, 0.00030159950256347656] -``` - -We can plot this again on the same input. - -```python -nelements = [10, 100, 1000, 10000, 100000] -bubbletimeSorted = [6.4373016357421875e-06, 1.9073486328125e-06, - 4.291534423828125e-06, 3.147125244140625e-05, 0.00030159950256347656] - -plt.title("Bubble Sort on an Already Sorted Array") -plt.xlabel("log(number of input)") -plt.ylabel("log(computation time (s))") -plt.plot(np.log(nelements), np.log(bubbletimeSorted),'o-') -``` - -![](/assets/images/week2/plot_time_bubblesort_sorted.jpeg) - -Taking the slope between 8 to 11, we have approximately the following slope: - -$$slope = \frac{(-8 - (-11))}{(11 - 8)} = 1$$ - -We don't take the first few data points because it is is limited by the accuracy of the floating point number in Python when the number is too small, e.g. $\approx 10^{-12}$. - -Notice that the computation time now falls in a straight line with a slope of about 1. This shows that when the input is already sorted, the computation time increases linearly instead of quadratically as the input increases. This means that in the best case scenario for the computation time is linear. - -$$T(n) = O(n)$$ - -### Insertion Sort - -We can do the same with Insertion Sort Algorithm. Below is the output when the input is randomly shuffled. - -```python -insertiontime = [6.198883056640625e-06, 7.867813110351562e-06, - 0.0006382465362548828, 0.06774091720581055, 6.839613199234009] -``` - -We can plot this with the same input. - -![](/assets/images/week2/plot_time_insertionsort_random.jpeg) - -We can again notice that the computation time increases in this logarithmic scales with a slope of about 2. This means that the computation time is also quadratic. - -For now, we can say that the computation time for Insertion Sort is quadratic. - -$$T(n) = O(n^2)$$ - -On the other hand, this is the output when the input is already sorted. - -```python -insertiontimeSorted = [5.7220458984375e-06, 1.430511474609375e-06, - 4.0531158447265625e-06, 3.123283386230469e-05, 0.0003333091735839844] -``` - -And if we plot, we will see the following. - -```python -nelements = [10, 100, 1000, 10000, 100000] -insertiontimeSorted = [5.7220458984375e-06, 1.430511474609375e-06, - 4.0531158447265625e-06, 3.123283386230469e-05, 0.0003333091735839844] - -plt.title("Insertion Sort on an Already Sorted Array") -plt.xlabel("log(number of input)") -plt.ylabel("log(computation time (s))") -plt.plot(np.log(nelements), np.log(insertiontimeSorted),'o-') -``` - -![](/assets/images/week2/plot_time_insertionsort_sorted.jpeg) - -Similarly, in this plot, looking at the x axis between 7 to 11, the slope is about 1 indicating that the computation time is linear. So the computation time when the input is already sorted is linearly increasing with the input numbers, similar to Bubble Sort. This means that in the best case scenario, the computation time for Insertion Sort is linear. - -$$T(n) = O(n)$$ - -### Heapsort - -We can now check the computation time for heapsort algorithm. The computation time for randomly shuffled array is as shown below. - -```python -heapsorttime = [5.0067901611328125e-06, 7.867813110351562e-06, - 9.512901306152344e-05, 0.0012400150299072266, - 0.015644311904907227, 0.21677017211914062] -``` - -A quick look actually shows that heapsort is much faster the other two. Let's plot it. - -```python -nelements = [10, 100, 1000, 10000, 100000, 1000000] -heapsorttime = [5.0067901611328125e-06, 7.867813110351562e-06, - 9.512901306152344e-05, 0.0012400150299072266, - 0.015644311904907227, 0.21677017211914062] - -plt.title("Heapsort on Randomly Shuffled Array") -plt.xlabel("log(number of input)") -plt.ylabel("log(computation time (s))") -plt.plot(np.log(nelements), np.log(heapsorttime),'o-') -``` - -![](/assets/images/week2/plot_time_heapsort_random.jpeg) - -We can see that the logarithmic plot has the slope of about: - -$$\frac{(-4-(-8))}{(12-8)} \approx \frac{4}{4} \approx 1$$ - -The slope is actually slightly greater than one if we compute accurately. We found out that the computation time is not linear and it is not quadratic. You can notice that the computation time for 1,000,000 input elements are only about 0.2 seconds. The other algorithms were too slow to compute this amount of input elements and so we only compute up to 100,000 elements. For 100,000 elements, Bubble Sort takes about 25 seconds while Insertion Sort takes about 7 seconds. Compare this with heapsort which only takes 0.016 seconds. - -It turns out that the computation time for Heapsort is logarithmic. We can see a linear relationship if the x-axis is computed as $n*log(n)$. We will plot the y axis as it is. See the plot below. - -```python -nelements = np.array([10, 100, 1000, 10000, 100000, 1000000]) -nlog = nelements * np.log(nelements) -heapsorttime = [5.0067901611328125e-06, 7.867813110351562e-06, - 9.512901306152344e-05, 0.0012400150299072266, - 0.015644311904907227, 0.21677017211914062] - -plt.title("Heapsort on Randomly Shuffled Array") -plt.xlabel("number of input * log(number of input)") -plt.ylabel("computation time (s)") -plt.plot(nlog, heapsorttime,'o-') -``` - -![](/assets/images/week2/plot_time_heapsort_random_xaxis.jpeg) - -Notice that now the points fall almost in a straight line. This means that the computation time for heapsort is: - -$$T(n) = O(n\log(n))$$ - -Now, what happens when we run the algorithm on an already sorted list? It turns out, that the computation time for different number of input elements are as follows. - -```python -heapsorttimeSorted = [1.2874603271484375e-05, 7.3909759521484375e-06, - 7.82012939453125e-05, 0.0008978843688964844, - 0.009733200073242188, 0.11059808731079102] -``` - -It turns out that the computation time is still - -$$T(n) = O(n\log(n))$$ - -We can plot this after modifying the x-axis accordingly. - -```python -nelements = np.array([10, 100, 1000, 10000, 100000, 1000000]) -nlog = nelements * np.log(nelements) -heapsorttimeSorted = [1.2874603271484375e-05, 7.3909759521484375e-06, - 7.82012939453125e-05, 0.0008978843688964844, - 0.009733200073242188, 0.11059808731079102] - -plt.title("Heapsort on an already Sorted Shuffled Array") -plt.xlabel("number of input * log(number of input)") -plt.ylabel("computation time (s)") -plt.plot(nlog, heapsorttime,'o-') -``` - -![](/assets/images/week2/plot_time_heapsort_sorted.jpeg) - -Let's summarize our findings in a table form. - -| Sorting Algorithm | Random List | Sorted List $T(n)$ | -|-------------------|---------------|---------------| -| Bubble Sort | $O(n^2)$ | $O(n)$ | -| Insertion Sort | $O(n^2)$ | $O(n)$ | -| Heapsort | $O(n\log(n))$ | $O(n\log(n))$| - -## Analysing Computation Time Using Model - -Given a computer program, can we analyse what is the computation time without resolving to the experimentation as we did in the previous section? In this section, we will introduce how we can analyse computer program by looking into the code and applying some computational time model. - -To do so, we are going to indicate using asymptotic notation. In particular we will use the big Oh notation for this purpose. - -### Bubble Sort Computation Time - -Let's take a look at the pseudocode version 1 for Bubble Sort. - -``` -Bubble Sort -Version: 1 -Input: array -Output: None, sort in place -Steps: -1. n = length of array -2. For outer_index from 1 to n-1, do: - 2.1 For inner_index from 1 to n-1, do: - 2.1.1 first_number = array[inner_index - 1] - 2.1.2 second_number = array[inner_index] - 2.1.3 if first_number > second_number, do: - 2.1.3.1 swap(first_number, second_number) -``` - -Computation time: - -* step 1: constant time, $O(1)$. -* steps 2: n-1 times, so $O(n-1) = O(n)$ - * step 2.1: n-1 times, so $O(n)$ - * step 2.1.1 to 2.1.3, are all constant time, so it's $3 \times O(1) = O(1)$ - -So we can compute the computation time for Bubble Sort as follows. - -$T(n) = O(1) + O(n) \times (O(n) \times( O(1)))$ - -This can be simplified to - -$T(n) = O(1) + O(n) \times O(n)$ - -$T(n) = O(1) + O(n^2)$ - -$T(n) = O(n^2)$ - -This agrees with the previous section when we state that computational time for Bubble Sort is quadratic. We can also say that the computation is polynomial time. - -Let's take a look at version 4 of Bubble Sort. - -``` -Bubble Sort -Version: 4 -Input: array -Output: None, sort in place -Steps: -1. n = length of array -2. swapped = True -3. As long as swapped is True, do: - 3.1 swapped = False - 3.2 new_n = 0 - 3.3 For inner_index from 1 to n-1, do: - 3.3.1 first_number = array[inner_index - 1] - 3.3.2 second_number = array[inner_index] - 3.3.3 if first_number > second_number, do: - 3.3.3.1 swap(first_number, second_number) - 3.3.3.2 swapped = True - 3.3.3.3 new_n = inner_index - 3.4 n = new_n -``` - -Computation time: - -* steps 1 and 2 are constant time, it's $2 \times O(1)$ or simply $O(1)$. -* step 3 is executed as long as there is a swap. In this case, we can consider the worst case scenario. In the worst case, the number of swap will be the same as $n-1$. In this case the computation time is $O(n)$. In the best case scenario, there is no swap because it is already sorted, and the computation time is $O(1)$. So on average, the computation time is $O(n/2)$, which is the same as $O(n)$. - * steps 3.1 and 3.2 are constant time, it's $O(1)$. - * step 3.3 is $n-1$ times, so the computation time is $O(n)$. - * steps 3.3.1 to 3.3.3 are all constant time, it's $O(1)$. - * step 3.4 is also constant time, $O(1)$. - - -We can calculate the computation time on *average* as: - -$T(n) = 2 \times O(1) + O(n) \times (2\times O(1) + O(n) \times (3\times O(1)) + O(1))$ - -This can be simplified to - -$T(n) = O(1) + O(n) \times(O(1) + O(n) + O(1))$ - -$T(n) = O(1) + O(n) \times O(n)$ - -$T(n) = O(1) + O(n^2) $ - -$T(n) = O(n^2)$ - -So the optimised Bubble Sort in version 4 is still polynomial time, which is quadratic. - - - -## Insertion Sort Computation Time - -Now, let's consider Insertion Sort computation time. We will use version 2 of Insertion Sort pseudocode. - -``` -Insertion Sort -Version: 2 -Input: array -Output: None, sort in place -Steps: -1. n = length of array -2. For outer_index in Range(from 1 to n-1), do: - 2.1 inner_index = outer_index # start with the i-th element - 2.2 temporary = array[inner_index] - 2.3 As long as (inner_index > 0) AND (temporary < array[inner_index - 1]), do: - 2.3.1 array[inner_index] = array[inner_index - 1]) # shift to the right - 2.3.2 inner_index = inner_index - 1 # move to the left - 2.4 array[inner_index] = temporary # save temporary to its final position -``` - -Computation time: - -* step 1 is $O(1)$ -* step 2 is executed $n-1$, so the time is $O(n)$. - * steps 2.1 and 2.2 are all constant time, so it is $O(1)$. - * step 2.3 is executed depending on the actual values in the array. In the worst case it is $n-1$ times, and in the best case it's already ordered and executed as constant time. In average, then the computation time is $O(n/2)$ or simply $O(n)$. - * steps 2.3.1 and 2.3.2 are constant time, i.e. $O(1)$. - * step 2.4 is also constant time, i.e. $O(1)$. - -We can calculate the computation time as follows. - -$T(n) = O(1) + O(n) \times (2 \times O(1) + O(n) \times(O(1)) + O(1) )$ - -$T(n) = O(1) + O(n) \times ( O(1) + O(n) + O(1) )$ - -$T(n) = O(1) + O(n) \times O(n) $ - -$T(n) = O(1) + O(n^2) $ - -$T(n) = O(n^2) $ - -So Insertion sort is similar to Bubble sort in its computational time, which is a polynomial time and it's quadratic relationship with respect to the number of input. - -## Heapsort - -Now, we will consider heapsort algorithm's computation time. The pseudocode can be written as follows: - -``` -def heapsort(array): -Input: - - array: any arbitrary array -Output: None -Steps: -1. call build-max-heap(array) -2. heap_end_pos = length of array - 1 # index of the last element in the heap -3. As long as (heap_end_pos > 0), do: - 3.1 swap( array[0], array[heap_end_pos]) - 3.2 heap_end_pos = heap_end_pos -1 # reduce heap size - 3.3 call max-heapify(array[from index 0 to heap_end_pos inclusive], 0) -``` - -Computation time: - -* step 1 depends on computation time for build-max-heap() algorithm. We'll come to this later. -* step 2 is constant time, i.e. $O(1)$ -* step 3 is done from $n-1$ , which is the last element in the array, down to 1, which is the second element. This means that it is executed $n-1$ times, so the computation time is $O(n)$. - * steps 3.1 and 3.2 are constant time, $O(1)$. - * step 3.3 depends on computation time for max-heapify(). - -To get the computation time for heapsort, we need to check what is the computation time for `build-max-heap()` and `max-heapify`. Let's start with the first one. - -### Computation Time for Build-Max-Heap - -``` -def build-max-heap(array): -Input: - - array: arbitrary array of integers -Output: None, sort the element in place -Steps: -1. n = length of array -2. starting_index = integer(n / 2) - 1 # start from the middle or non-leaf node -3. For current_index in Range(from starting_index down to 0), do: - 3.1 call max-heapify(array, current_index) -``` - -Computation time: - -* step 1 and 2 are constant time, $O(1)$. -* step 3 is fixed and executed for $n/2$ times. We can say that the computation time is $O(n)$. -* step 3.1 on the other hand depends on the computation time for `max-heapify`. - -### Computation Time for Max-Heapify - -``` -def max-heapify(A, i): -version: 2 -Input: - - A = array storing the heap - - i = index of the current node to restore max-heap property -Output: None, restore the element in place -Steps: -1. current_i = i # current index starting from input i -2. swapped = True -3. As long as ( left(current_i) < length of array) AND swapped, do: - 3.1 swapped = False - 3.2 max_child_i = get the index of largest child of the node current_i - 3.3 if array[max_child_i] > array[current_i], do: - 3.3.1 swap( array[max_child_i], array[current_i]) - 3.3.2 swapped = True - 3.3 current_i = max_child_i # move to the index of the largest child -``` - -Computation time for `max-heapify()`: -* step 1 and 2 are constant time, $O(1)$. -* step 3, in average is executed in $O(\log(n))$ time. - * steps 3.1 to 3.4 are all constant time, $O(1)$. - * steps 3.3.1 and 3.3.2 are also constant time, $O(1)$. - - - -In this case, computation time is: - -$T(n) = O(1) + O(\log(n)) \times (O(1) \times O(1))$ - -$T(n) = O(\log(n))$ - -### How do we get the $\log(n)$ time for step 3? - -You can skip this part if you want. But for those curious, let's represent some example of binary tree and calculate the number of levels and its nodes or elements. In the below table, we represent the nodes at different levels with different symbols. For example, when there are 3 levels (level 2 row in the table), the only node in level 0 is represented as `o`, level 1 nodes are represented as `x` (there are two of them), and level 2 nodes are represented as `o` again (there are four of them, two for each `x` node in level 1). - -| level | diagram (different level using different symbol) | nodes at level i | total number of nodes | -|-------|---------------------------|--------------------|-------------------------| -| 0 | o | 1 | 1 | -| 1 | o xx | 2 | 3 | -| 2 | o x x oo oo | 4 | 7 | -| 3 | o x x oo oo xx xx xx xx | 8 | 15 | - -From the above table we can see that the maximum number of elements at level *i* can be calculated from the level position as - -$$n_{i} = 2^i$$ - -For example, at level $i=3$, the maximum number of elements can be up to $n = 2^3 = 8$. The total number of elements can, therefore, be calculated from - -$$n = \sum^{i_{max}}_{i = 0} 2^i$$ - -In a geometric series, we can find the closed form of the summation as follows. - -$$a + ar + ar^2 + \ldots + ar^{n-1} = \frac{a(1-r^n)}{1-r}$$ - -In our summation for the number of nodes $n$ in a binary heap, $a=1$ and $r=2$, and the number of terms is $i_{max}+1$. Therefore, - -$$n = \frac{(1-2^{i_{max}+1})}{1-2}$$ - -$$n = 2^{i_{max}+1}-1$$ - -For example, if there are three levels, $i_{max} = 3$, then the maximum total number of elements is $n = 2^4-1 =16-1=15$. - -This also means that we can get the number of levels from the number of elements in the tree by doing its inverse function as follows. - -$$level =\lfloor{ \log_2(n)}\rfloor$$ - -In computing, we usually understand that the base of the log is 2. Therefore, we usually omit the base in our equation. - -$$level =\lfloor{ \log(n)}\rfloor$$ - -Let's come back to step 3. This step is executed by moving `current_i` from one node to another node. The movement is always from the parent to the child node. This means that it moves in the vertical direction in the binary tree across the levels. Since there are $\log(n)$ levels, the maximum number of moves will be $\log(n)-1$. That's why we say that the computation time is $O(\log(n))$. - -### Total Computation Time for Heapsort - -Let's recall and summarize the computation time of the different procedures. - -* Heapsort - - $T(n) = T_{build-max-heap}(n) + O(1) + O(n) \times(O(1)+ T_{max-heapify}(n))$ - - $T(n) = T_{build-max-heap}(n) + O(n) \times T_{max-heapify}(n)$ - -* Build-Max-Heap - - $T_{build-max-heap}(n) = O(1) + O(n) \times T_{max-heapify}$ - - $T_{build-max-heap}(n) = O(n) \times T_{max-heapify}$ - -* Max-Heapify - - $T_{max-heapify}(n) = O(\log(n))$ - -From this we can calculate that the computation time for `build-max-heapify()` is - -$T_{build-max-heap}(n) = O(n) \times O(\log(n))$ - -$T_{build-max-heap}(n) = O(n\log(n))$ - -We can then substitute this to the computation time for Heapsort to get - -$T(n) = O(n\log(n)) + O(n) \times O(\log(n))$ - -$T(n) = O(n\log(n)) + O(n\log(n))$ - -$T(n) = O(n\log(n)) $ - -# Computational Time Summary - -In summary, different algorithm may have different performance in terms of computational time. The following image shows the different plots for some common computational time in computing. - -Trulli - -In our examples above, both Bubble Sort and Insertion sort is quadratic while Heapsort is log linear. We can see that Heapsort is much faster as the number of elements grows big. diff --git a/_Notes/Computational_Thinking.md b/_Notes/Computational_Thinking.md new file mode 100644 index 0000000..2681012 --- /dev/null +++ b/_Notes/Computational_Thinking.md @@ -0,0 +1,55 @@ +--- +title: What is Computational Thinking +permalink: /notes/intro-ct +key: notes-intro-ct +layout: article +nav_key: Notes +sidebar: + nav: Notes +license: false +aside: + toc: true +show_edit_on_github: false +show_date: false +--- + +## What is Computational Thinking? +Before we delve into programming, we would like to talk first about computational thinking. Computational thinking has been identified as one of the important work skils in the 21st century. A number of governments have revised their primary education curriculum to include computational thinkiing for all students. Some argue that computational thinking should be another literacy that must be taught similar to writing and mathematics. So what is computational thinking and how it relates to programming? + +Wing defines computational thinking as taking an approach to solving problems, designing systems and understanding human behaviours that draws on the concepts fundamental to computing[^1]. Computational thinking's essence is thinking like a computer scientist when confronted with a problem. Computational thinking in a way changes the way we *think*. Computational concepts provide a new language for describing problems and their solutions. To solve problems in the future, we will need to understand computation. + +[^1]:
Wing, J. M. (2006). Computational Thinking. Communications of the ACM, 49(3), 33โ€“35.
+ +Wing tried to clarified her own definition in saying that "Computational thinking is the thought processes involved in formulating problems and their solutions so that the solutions are repsented in a form that can be effectively carried out by an information-processing agent"[^2]. This is a rather general statements, but there are a few components highlighted by this definition: +- "thought processes": Computational thinking deals more with the thought processes or the thinking process rather than codes and computer programs. This is why such thinking skills is important for a wider audience than just those who wants to be a programmers. +- "formulating problems and their solutions": Computational thinking focuses on how to solve problems. It is not the ultimate thinking skills needed but rather only one of the thinking skills that deals with problem solving. Mathematical thinking may provide a different point of few of how to solve problems. Design thinking skills may help someone to generate more ideas and design a solution that emphatise with the users. Yet, computational thinking focuses on problem solving and in a specific way. +- "solutions are represented in a form that can be effectively carried out by an information-processing agent": This particular emphasis separates computational thinking with any other problem solving thought process. It's a mouthful and some people have tried to simplify those words. Aho simplified it to "solutions that can be represented as computational steps and algorithms"[^3] (Aho 2012). This is how computational thinking is closely related to programming. + +[^2]: https://www.cs.cmu.edu/link/research-notebook-computational-thinking-what-and-why + +[^3]:
Aho, A. v. (2012). Computation and Computational Thinking. The Computer Journal, 55(7), 832โ€“835. https://doi.org/10.1093/COMJNL/BXS074
+ +## Components of Computational Thinking + +There are many attempts to identify the elements of computational thinking. For example, Grover listed the following as important elements in computational thinking[^4]: +- abstraction and pattern generalization +- systematic processing of information +- symbol systems and representations +- algorithmic notions of flow control +- structured problem decomposition (modularizing) +- iterative, recursive and parallel thinking +- conditional logic +- efficiency and performance constraints +- debugging and systematic error detection + +[^4]:
Grover, S., & Pea, R. (2013). Computational Thinking in Kโ€“12. Educational Researcher, 42(1), 38โ€“43. https://doi.org/10.3102/0013189X12463051
+ +Some other authors prefer a simpler four main components of computational thinkings which can be abreviated as DAPA. +- Decomposition +- Abstraction +- Pattern Recognition +- Algorithms + +This notes will follow the later for simplicity and will try to apply these through Python programming language. Python is chosen for its simplicity but any other programming language can be used to build up your computational thinking skills. + + diff --git a/_Notes/Confusion_Matrix_Metrics.md b/_Notes/Confusion_Matrix_Metrics.md deleted file mode 100644 index 1555950..0000000 --- a/_Notes/Confusion_Matrix_Metrics.md +++ /dev/null @@ -1,169 +0,0 @@ ---- -title: Confusion Matrix and Metrics -permalink: /notes/confusion_matrix_metrics -key: notes-confusion-matrix-metrics -layout: article -nav_key: Notes -sidebar: - nav: Notes -license: false -aside: - toc: true -show_edit_on_github: false -show_date: false ---- - - -In Linear Regression, we use the correlation coefficient and some mean square errors as metrics to see if our model fits the data well. What kind of metrics we can use in the case of classification problems? In this lesson we will use confusion matrix and a few matrix to evaluate our classification model. - - - -## Confusion Matrix - -Recall in the first lesson on machine learning, we give an example of classifying image as cat or not a cat. Now, let's say we have a dataset of images of various animals including some cats picture inside. First, we need to separate the dataset into training set and test set. The training set is used to build the model or to train the model. Our model for classification which we discussed in the previous lessson was called Logistic Regression. After we train the model, we would like to measure how good the model is using the **test set**. We can write a table with the result of how many data is predicted correctly and not correctly as shown below. - -| actual\predicted | a cat | not a cat | -|------------------|-------|-----------| -| a cat | 11 | 3 | -| not a cat | 2 | 9 | - -The above table gives some example of what is called as a **confusion matrix**. The vertical rows are the labels for the **actual** data while the horizontal columns are the labels for the **predicted** data. We can read this table as follows. -- Out of all the *actual* image which is *a cat*, 11 images are *predicted* as *a cat* and 3 images are *predicted* as *not a cat*. This means that 11 of them are accurate and 3 of them is not. -- Out of all the *actual* image which is *not a cat*, 2 images are *predicted* as *a cat* and 9 images are *predicted* as *not a cat*. This means that 2 of them are not accurate and 8 is. - - -We can see that there are a total of $11+3=14$ images of the category *a cat*. On the other hand, there are $2+9=11$ images of the category *a cat*. So the total number of images are $11 + 3 + 2 + 9 = 25$ images. - -The confusion matrix in general is written as follows. - -| actual\predicted | Positive Case | Negative Case | -|------------------|-----------------|-----------------| -| Positive Case | True Positives | False Negatives | -| Negative Case | False Positives | True Negatives | - -We use the term **positive** case here to refer to the category of point of interest. In this case, we would like to detect a cat and so category *a cat* is a positive case. On the other hand, the category *not a cat* is a negative case. This means that: -- There are 11 True Positive cases where the actual cat images are predicted as a cat. -- There are 9 True Negative cases where the actual not a cat images are predicted as not a cat. -- There are 3 False Negative cases where the actual cat images are predicted as *not a cat* (negative case). -- There are 2 False Positive cases where the actual not. acat images are predicated as *a cat* (positive case). - - -## Metrics - -Knowing the confusion matrix allows us to compute several other metrics that is useful for us to evaluate our model. - -### Accuracy - -In this metrics, we are interested in how many data is predicted correctly. In the above examples, we have 11 images predicted correctly as cats and 9 images predicted correctly as not a cat. Therefore, we have the accuracy of: - -$$\text{accuracy} = \frac{11 + 9}{25} = \frac{20}{25} = 0.8$$ - -This means our model have 80% accuracy. In general, the accuracy formula can be written as: - -$$\text{accuracy} = \frac{\text{TP} + \text{TN}}{\text{Total Cases}}$$ - -where TP is the number of True Positive cases, TN is the number of True Negative cases. You can also see accuracy as a fraction of the green circle over the blue circle in the image below. - -drawing - -Given the accuracy, we can also calculate another metric called **error rate**. In fact, the error rate is just given by the following: - -$$\text{error rate} = 1 - \text{accuracy}$$ - -So in our example, the error rate is $1 - 0.8 = 0.2$ which is 20%. - -### Precision - -In precision, we put more attention into the positive cases. We are interested in how many of the positive cases are detected accurately. - -| actual\predicted | a cat | not a cat | -|------------------|-------|-----------| -| a cat | 11 | 3 | -| not a cat | 2 | 9 | - -$$\text{precision} = \frac{11}{11 + 2} = \frac{11}{13} = 0.846$$ - -This means that we have a precision of about 85\%. Out of all the total 13 cases detected positive, 11 of them is correct. Obviously we want our precision to be as high as possible. We can write precision formula as follows. - -$$\text{precision} = \frac{\text{TP}}{\text{Total Predicted Positives}} = \frac{\text{TP}}{\text{TP} + \text{FP}}$$ - -The calculation of precision can be seen as a fraction between the green circle and the blue circle in the image below. - -drawing - -### Sensitivity - -This is also often called as Recall. In this metric, we are interested to know the fraction of the number of positive cases predicted accurately out of all the *actual* positive cases. The difference with precision is that precision is calcalculated as a fraction out of all the total predicted positive cases. In the above example, we have 11 true positives and the total number of all actual positive cases are $11 + 3 = 14$. - -| actual\predicted | a cat | not a cat | -|------------------|-------|-----------| -| a cat | 11 | 3 | -| not a cat | 2 | 9 | - -$$\text{sensitivity} = \frac{11}{11 + 3} = \frac{11}{14} = 0.786$$ - -This means that the sensitivity is about 79\%. Notice that the sensitivy value is different from precision which is about 85\%. We can write the formula for sensitivity as follows. - -$$\text{sensitivity} = \frac{\text{TP}}{\text{Total Actual Positives}} = \frac{\text{TP}}{\text{TP} + \text{FN}}$$ - -We can see sensitivty as a fraction between the green circle and the blue circle in the image below. - -drawing - - -### Specificity - -Another common matrix is called specificity. This is the metrix we normally use when we are interested in the negative cases. It is also called as *True Negative Rate*. Specificity is calculated as the number of True Negative cases divided over all actual Negative cases. - -| actual\predicted | a cat | not a cat | -|------------------|-------|-----------| -| a cat | 11 | 3 | -| not a cat | 2 | 9 | - -$$\text{specificity} = \frac{9}{9 + 2} = \frac{9}{11} = 0.818$$ - -This means that the true negative rate is about 82\%. You can see specificity as the sibling of sensitivity. In sensitivity, you are interested in the positive cases while in the specificity you are interested in the negative cases. The formula for specificity is given as follows. - - -$$\text{specificity} = \frac{\text{TN}}{\text{Total Actual Negatives}} = \frac{\text{TN}}{\text{FP} + \text{TN}}$$ - -We can see specificity as a fraction between the green circle and the blue circle in the image below. - -drawing - -## Confusion Matrix for Multiple Classes - -What if we have more than two categories. What would the confusion matrix look like? Let's say we are classifying images of cat, dog, and a fish. In this case, we have three categories. We can write our confusion matrix in this manner. - -| actual\predicted | cat | dog | fish | -|------------------|-----|-----|------| -| cat | 11 | 1 | 2 | -| dog | 2 | 9 | 3 | -| fish | 1 | 1 | 8 | - -The diagonal element again gives us the accuracy of the model. - -$$\text{accuracy} = \frac{11 + 9 + 8}{38} = \frac{28}{38} = 0.737$$ - -which is about 73\%. We can write the formula for accuracy as follows. - -$$\text{accuracy} = \frac{\sum_i M_{ii}}{\sum_i\sum_j{M_{ij}}} $$ - -The formula simply sums up all the diagonal elements and divide it with the sum of all. - -We can calculate the other metrices by defining our positive case in a *one-versus-all* manner. This means that if we define cat as our positive case, we define both dog and fish as the negative cases. - -For example, to calculate the sensitivity for class *i*, we use: - -$$\text{sensitivity}_i = \frac{M_{ii}}{\sum_j{M_{ij}}}$$ - -where $i$ is the class we are interested. The summation over $j$ means we sum **over all the columns** in the confusion matrix. - -Similarly, we can calculate the precision for class *i* using: - -$$\text{precision}_i = \frac{M_{ii}}{\sum_j{M_{ji}}}$$ - -**Notice that the indices are swapped for the denominator**. For precision, instead of summing over all the columns, we **sum over all the rows** in column *i* which is the total cases when class *i* is *predicted*. - - - diff --git a/_Notes/Defining_Function.md b/_Notes/Defining_Function.md new file mode 100644 index 0000000..7169dcb --- /dev/null +++ b/_Notes/Defining_Function.md @@ -0,0 +1,600 @@ +--- +title: Defining a Function +permalink: /notes/defining-function +key: notes-defining-function +layout: article +nav_key: Notes +sidebar: + nav: Notes +license: false +aside: + toc: true +show_edit_on_github: false +show_date: false +--- + + +## Abstracting a Problem as a Function +In our previous lessons, we have been using some built-in functions such as `print()`, `int()` and `input()`. We also have started using other functions provided by `math` library, particularly the `sqrt()` function. There are many other functions provided in the `math` library. However, our focus now is on how can we create our own user-defined function or our own custom function. Let's say, what if you want to create a function to compute the cadence of your cycling app. Recall previously, that we wrote the following code in our cycling chatbot. + +```python +steps_inp: str = input("how many push you did on the pedal within 30 seconds? ") + +steps: int = int(steps_inp) +cadence: int = steps * 2 + +print("Your cadence is ", cadence) +``` + +Now, some other people may wonder why you multiply steps by 2. Wouldn't it be more easier to understand if you replace that line with the following? + +```python +cadence: int = compute_cadence_for_30sec(steps) +``` + +How cadence is computed does not matter at the moment for others to understand your code. See the overall code now. + +```python +steps_inp: str = input("how many push you did on the pedal within 30 seconds? ") + +steps: int = int(steps_inp) +cadence: int = compute_cadence_for_30sec(steps) + +print("Your cadence is ", cadence) +``` + +This new function `compute_cadence_for_30sec()` serves several purposes. First, it serves as an abstraction on how to calculate a cadence for 30 sec. People need not know how to actually compute cadence, they just need to call this function and they get the result. Second, by naming the process of computation `steps * 2` as `compute_cadence_for_30sec()`, we actually gives meaning to the overall process. Now calculating cadence is straight forward, but imagine if your calculation takes hundreds of lines. By naming those hundred of lines of code into a single name, we actually simplify code. We provide name and meaning to the overall process with a single name, i.e. the function name. + +But now, we need to be able to write our own custom function or user-defined function. Let's learn how to do it for the various possible function combination that you have seen previously. + + + +## Defining a User-Defined Function With Input Arguments and Return Values + + + +We will start with the first function that takes in input argument and return some output (return value). The format to define a new function is as follows. + +```python +def function_name(arg1, arg2, ...): + // body of the function + return output +``` + +Our `compute_cadence_for_30sec()` is an example of this kind of function. In this function, we take in the number of steps as our input and it returns the cadence as the output. Let's see how, we can define this function in Python. + +```python +def compute_cadence_for_30sec(steps): + return steps * 2 +``` + +## Specifying Data Types in Arguments and Return Values + +It is always good to ask the question, "What is the input?" and also "What is the output?". This is part of our PCDIT framework of Problem Statement. We can apply that framework to design our user-defined function. In this problem, our input is steps in 30 seconds and the output expected is the cadence. + +In the above code, `steps` is the only input argument. If you have more than one input arguments, you can separate them using a comma. The output of the function is specified using a special keyword `return`. In this case the output is this `steps * 2` which is the cadence. + +In asking the input and output of our problem statement, we must also ask, "What is the data type?" or "What kind of data is this?". The input `steps` is most probably a whole number and so we can use `int` data to represent steps. Similarly, since the cadence calculation is just a multiplication by 2, we would expect that the output is also an `int`. + +Python 3.6 onwards allows you to annotate the input and output data type for static type checker to verify. The above function definition can be re-written as follows. + +```python +def compute_cadence_for_30sec(steps:int) -> int: + return steps * 2 +``` + +Similar to declaring the type of a variable, we also declare the input argument's type in the same way using colon `:`, i.e. `steps:int`. As regards the output data type or the return value, we use arrow `-> int` to specify the expected data type. + +To use this function, we have to *invoke* or *call* the function. Let's write the code in a file called `01_cadence_int.py`. + +```python +def compute_cadence_for_30sec(steps: int) -> int: + return steps * 2 + +print(compute_cadence_for_30sec(25)) +``` + +We can run a static type checker on this file. + +```sh +$ mypy 01_cadence_int.py +Success: no issues found in 1 source file +``` + +Try to change the input argument to other data type such as a float and save it as a new file called `02_cadence_float.py`. + + +```python +def compute_cadence_for_30sec(steps: int) -> int: + return steps * 2 + +print(compute_cadence_for_30sec(24.5)) +``` + +You can see how `mypy` helps us to check if an incorrect input data type is given. + +```sh +$ mypy 02_cadence_float.py +02_cadence_float.py:4: error: Argument 1 to "compute_cadence_for_30sec" has incompatible type "float"; expected "int" [arg-type] +Found 1 error in 1 file (checked 1 source file) +``` + +The error says that argument 1 of the function expected an `int` but it is given a `float`. You may think, what's the big deal of checking the data type. In fact, if it is a float, you still get the answer correctly. Let's run Python on this file. + +```sh +$ python 02_cadence_float.py +49.0 +``` + +The function still computes correctly. But now, let's try what happens if you supply a `string` data into the function. Let's modify into a string and save it into a new file called `03_cadence_str.py`. + +```python +def compute_cadence_for_30sec(steps: int) -> int: + return steps * 2 + +print(compute_cadence_for_30sec("25")) +``` + +In the above code, the input argument provided is a `string` data, i.e `"25"`. Running this code results in the following. + +```sh +$ python 03_cadence_str.py +2525 +``` + +The output is puzzling and unexpected. It no longer gives `50` as the cadence as in the previous calculation. The reason is that the `*` operator works differently for different kind of data. For number data such as `int` and `float`, the operator `*` does multiplication. However, for `string` data, the operator `*` does a *duplication* of the string. In the above code, what happens is that the string `25` is duplicated *two times* when executing `return steps * 2`. + +It is important, therefore, to ask "What is the data type?". Using `mypy` would prevent such error. Notice that Python continues to execute the code without throwing any error. Python does not check the type. This is where running static type checker such as `mypy` helps. + +```sh +$ mypy 03_cadence_str.py +03_cadence_str.py:4: error: Argument 1 to "compute_cadence_for_30sec" has incompatible type "str"; expected "int" [arg-type] +Found 1 error in 1 file (checked 1 source file) +``` + +## Functions Taking in Multiple Arguments + +If you know your bicycle parameters, you can determine your cycling speed based on your cadence and those parameters. Using the below equation, you can determine the speed. + +$$\text{speed} = \pi \times \left(\text{diameter} + (2 \times \text{tire\_size})\right) \times (\text{chainring}/\text{cog}) \times \text{cadence}$$ + +In the above equation, we need a few input values in order to determine the speed: +- diameter of the wheel in mm +- tire size in mm +- chainring and cog values. The ratio between them is called the gear ratio. +- and lastly the cadence. + +Now, if we want to compute the speed, we need all these inputs and our functions must be able to take in all these inputs. In Python, we separate the different input arguments using a comma as shown below. + +```python +import math +def calculate_speed(diameter, tire_size, chainring, cog, cadence): + gear_ratio = chainring / cog + speed = math.pi * (diameter + (2 * tire_size)) * gear_ratio * cadence + return speed * 60 / 1_000_000 +``` + +Notice that we have imported the `math` library in order to get the value of $\pi$. We have talked about math library in our previous lessons where we imported some functions. This time, instead of a function, we imported a constant from the library. + +We also have a few input arguments this time. In fact, we have five of them. It is useful to document what these arguments are. We may need it to describe the unit of these values such as whether they are in milimeters or in inches. Similarly what is the unit of the output. The last line of the above functin is needed to ensure that the output speed has a unit in km/h. + +Note that the above code is just a **function definition**. We have not executed the function. To do a function invocation or function call, we need to type in the function name and provide the **actual arguments**. Let's input the following input arguments: +- wheel's diameter to be 685.8 mm or 27 inches +- tire's size to be 38.1 mm or 1.5 inches +- chainring to be 50 teeth +- cog to be 14 teeth +- and lastly the cadence is 25 rpm. + +```python +>>> print(calculate_speed(685.8, 38.1, 50, 14, 25)) +12.824430010904047 +``` + +The result of the calculation is about 12.8 km/h for the given input arguments. + +As you may see that the output calculated depends on the unit of the inputs. For someone to use this function, they need to know what kind of value to be put into the actual arguments. + +The common way to do it is to insert what we call as a docstring. It is a multiline of string just after the function definition which serves to explain about the function. Let's write our docstring describing the input and the output. + +```python +import math +def calculate_speed(diameter, tire_size, chainring, cog, cadence): + ''' + Calculates the speed from the given bike parameters and cadence. + + Parameters: + diameter (float): diameter of the wheel in mm + tire_size (float): size of tire in mm + chainring (int): teeth of the front gears + cog (int): teeth of the rear gears + cadence (int): cadence in rpm + + Returns: + speed (float): cycling speed in km/h + ''' + gear_ratio: float = chainring / cog + speed: float = math.pi * (diameter + (2 * tire_size)) * gear_ratio * cadence + return speed * 60 / 1_000_000 +``` + +Since the input arguments are all in mm and the cadence is in rotation per *minute*, the variable `speed` has a unit of mm/min. In order to change this unit to km/h, we multiply by 60 (to convert from minute to hour) and divide by 1,000,000 (to convert from mm to km). Notice that we expect the `gear_ratio` to be a float after dividing `chainring` by `cog`. Similarly, `speed` is expected to be a `float`. + +The nice thing about Python docstring is that we can access this documentation. To do that, type in the above code into the Python shell and press enter for Python to execute the function definition. In order to access the documentation, you can type `calculate_speed.__doc__`. + +```python +>>> print(calculate_speed.__doc__) + + Calculates the speed from the given bike parameters and cadence. + + Parameters: + diameter (float): diameter of the wheel in mm + tire_size (float): size of tire in mm + chainring (int): teeth of the front gears + cog (int): teeth of the rear gears + cadence (int): cadence in rpm + + Returns: + speed (float): cycling speed in km/h +``` + +You can also type `help(calculate_speed)` and it will enter into a help documentation mode in Python. + +``` +Help on function calculate_speed in module __main__: + +calculate_speed(diameter, tire_size, chainring, cog, cadence) + Calculates the speed from the given bike parameters and cadence. + + Parameters: + diameter (float): diameter of the wheel in mm + tire_size (float): size of tire in mm + chainring (int): teeth of the front gears + cog (int): teeth of the rear gears + cadence (int): cadence in rpm + + Returns: + speed (float): cycling speed in km/h +``` + +We can include the data type as part of the annotation into our function definition in order to make it even better. Our final function definition for the `calculate_speed()` is as shown below. + +```python +import math +def calculate_speed(diameter: float, tire_size: float, + chainring: int, cog: int, + cadence: int) -> float: + ''' + Calculates the speed from the given bike parameters and cadence. + + Parameters: + diameter (float): diameter of the wheel in mm + tire_size (float): size of tire in mm + chainring (int): teeth of the front gears + cog (int): teeth of the rear gears + cadence (int): cadence in rpm + + Returns: + speed (float): cycling speed in km/h + ''' + gear_ratio: float = chainring / cog + speed: float = math.pi * (diameter + (2 * tire_size)) \ + * gear_ratio * cadence + return speed * 60 / 1_000_000 +``` + +In the above definition, we decided to write the arguments in multiple lines. Python can take this as it understands that the arguments have to be enclosed within the parenthesis. We have also used the backslash `\` character to write our long math expression into two lines. It is a convention to break *before* the binary operator following [PEP 8](https://peps.python.org/pep-0008/#should-a-line-break-before-or-after-a-binary-operator). + +## Functions Returning Multiple Outputs Using a Tuple + +Let's say if we want our function not only to return the speed in km/h but also in mph (miles per hour). Python allows functions to return a *tuple*. A tuple is a collection data type that can contain more than one values. In order to create a tuple, we just need to separate them using a comma (`,`). Most of the time, for clarity purposes, we put a parenthesis around the tuple. + +For example, the code below creates a tuple and print its values and its type. + +```python +>>> my_output:tuple = 12.8, 7.97 +>>> print(my_output) +(12.8, 7.97) +>>> print(type(my_output)) + +``` + +We can then use a tuple to return multiple values out of the function. For example, the first value of the tuple can be the speed in km/h and the second value of the tuple is the speed in mph. We can rewrite our functions as shown below. + +```python +import math +def calculate_speed(diameter: float, tire_size: float, + chainring: int, cog: int, + cadence: int) -> tuple[float, float]: + ''' + Calculates the speed from the given bike parameters and cadence. + + Parameters: + diameter (float): diameter of the wheel in mm + tire_size (float): size of tire in mm + chainring (int): teeth of the front gears + cog (int): teeth of the rear gears + cadence (int): cadence in rpm + + Returns: + Tuple: + speed_kmph (float): cycling speed in km/h + speed_mph (float): cycling speed in km/h + ''' + gear_ratio: float = chainring / cog + speed: float = math.pi * (diameter + (2 * tire_size)) \ + * gear_ratio * cadence + speed_kmph: float = speed * 60 / 1_000_000 + speed_mph: float = speed * 60 / 1.609e6 + return speed_kmph, speed_mph +``` + +In our equation above, we created two additional variables to store the speed in km/h and the speed in mph. We use a conversion of 1 mile to be about $1.609\times 10^6 \text{mm}$. + +There are two ways to access the output. First is that, we provide two variables to store the output when calling the function. + +```python +speed_kmph, speed_mph = calculate_speed(685.8, 38.1, 50, 14, 25) +print(speed_kmph, speed_mph) +``` + +The other way is to store it in a single variable first which then can be accessed using the bracket operator. + +```python +speed: tuple = calculate_speed(685.8, 38.1, 50, 14, 25) +print(speed[0], speed[1]) +``` + +The bracket operator takes in an index which starts from 0. This means that to access the first output, you use index 0. On the other hand, to access the second output, you use index 1 and so on. + +You can access the code above in the file `05_speed_annotated.py` and run `mypy` to do static check on it. + +## Local Frame and Local Variables + +At this point it is instructive to see the difference between a global and local variables as this may affect the way we define our functions. Let's run the above functions on Python Tutor. + + + + +We have added the function call and the print statement at the end of the code. We have also removed the docstring to shorten the code. At the same time, we created an Alias to define the type of the return value of the function. + +```python +Speed = tuple[float, float] +``` + +We can then define `Speed` as a tuple that consists of two float values. We then use this `Speed` to annotate the return value of the function. + +```python +def calculate_speed(diameter: float, tire_size: float, + chainring: int, cog: int, + cadence: int) -> Speed: +``` + +Since `speed` is a tuple, we can access the elements using the square bracket operators. + +```python +speed: Speed = calculate_speed(685.8, 38.1, 50, 14, 25) +print(speed[0], speed[1]) +``` + +Click the "Next" button a few times until the program counter reach line number 11, which is step 11 out of 13. + +At this point the image on the right hand side gives a snapshot of what is in the memory before the function exit and returns the two values. A few things which you need to take note. The first one is that there are **two frames** created. The first one is the **global** frame and the second one is the **calculate_speed** frame. The calculate speed frame is created when the function `calculate_speed` is called. This happens on step 6 of 13. You can click the "Prev" button to verify this. Go to step 5 and click "Next" to see when is the `calculate_speed` frame created. In essence the `calculate_speed` frame is a local frame which is called when the function is *invoked* or *executed*. Every function invocation creates a new frame. + +At step 7, the program counter is at the first line of the code in function `calculate_speed`. At this point, the local frame has been populated by the input arguments: `diameter`, `tire_size`, `chainring`, `cog` and `cadence`. When you execute line 6 to arrive at Step 8, a new **local** variable is created, which is `gear_ratio`. When this local variable is created, several things happens: +- Python tries to evaluate the value of the expression on the right. +- On the right hand side of the assignment operator (`=`), Python sees the operator `/` and understood it as a division. Since this is a *binary* operator it requires two operands. +- Python found two **names** for the operands of `/`: `chainring` and `cog`. Python tries to see if these names are any of the built-in names or keywords. +- Since Python cannot find any of these names in its built-in functions or keywords, Python looks into the **local frame**. At this point, Python finds the two names and *evaluates* the values. +- Once Python obtains the two values for the operands, Python executes the division operation on the two operands and assign it to the name on the left hand side of the assignment operators, i.e. `gear_ratio`. + +In subsequent lines, Python does similar things to create several other *local variables*: `speed`, `speed_kmph` and `speed_mph`. + +Those are called **local variables** because they are local to the functin and are **not accessible** anywhere else. To see this, make sure your program counter is back at line 11 or Step 11 of 13. Click "Next" two times to exit the function. Notice that now there is only one single frame, which is the *global* frame. The local frame of `calculate_speed` has been destroyed. Since this frame is destroyed none of those local variables are accessible when we exit the function. + +## Global Variable + +Now, you may realize that most of the arguments in the function `calculate_speed` will not change if the bicycle remains the same. Most of these are parameters of the bicycle of the users. As long as the user does not change the bicycle, those parameters will not change. Do we need to keep on entering those numbers such as the diameters and the tire size again every time we want to calculate the speed for a given cadence? The answer is no. In this section we will show two alternatives which uses the global variable and optional arguments. We will discuss its advantage and disadvantage. In the future lessons, we will show some other alternatives such as using your own custom data type through object oriented programming. Object oriented programming will allow you to pass on the bicycle information as one single data. + +Let's start with using the global variables. Since these parameters never (or seldom) change, we may put all these values as a kind of constants and define it outside of the functions. In this way, all these constants are readable by all the functions that we create. How to do this? Let's see the code below in Python Tutor. + + + + +In the code above, we define all the constants the first few lines of the code followed by the function definition. Notice that now the functin definition only takes in `cadence` as the input argument. During function invocation, we also only provide one actual argument, which is for the cadence, i.e. 25. How does the function `calculate_speed` obtains the values from the constants? Click "Next" button until you reach the first line inside the function `calculate_speed`, i.e. Step 11 of 17. + +Notice the environment diagram on the right hand side. As expected, when we execute a functin, Python will create a local frame for that function. Here we see there is one local variable which comes from the input argument, i.e. `cadence`. However, note that the global frame has more names associated with it. This time, we not only have `math` but also `diameter`, `tire_size`, `chainring` and `cog`. We also have the name `calculate_speed` (Python needs to know how to execute this function). + +Let's see what happens when you execute line 13: +- At line 13, when we execute `gear_ratio = chainring / cog`, Python recognizes that you are doing assignment from the assignment operator, i.e. `=`. Because of this, Python tries to evaluate the right hand side of the expression. +- At the right hand side, Python sees the binary operator `/` and found two operands with two names `chainring` and `cog`. +- Python tries to find what these names are. Python cannot find these names in its built-in functions or keywords and so Python check the *local frame*. However, Python cannot find these two names in the local frame as well. +- Since Python, cannot find the names in the local frame, Python goes **up** one level to the **previous** frame. This is the frame where the function `calculate_speed` is called. It happens that this is the same as the *global* frame. At this point, Python finds the two names and evaluates the values and execute the binary operator to perform division. +- The result is assigned to a new name `gear_ratio` which is part of the *local* frame. + +Click Next to see how the environment change in the local frame. + +Similar things happens when we execute line 14 to calculate the `speed`. This time, Python gets the value for `diameter`, `tire_size` from the *global* frame. On the other hand, Python gets the value of `gear_ratio` and `cadence` from the local frame. You may not noticed that actually in this expression, we access the constant `math.pi` even in our earlier version of the code. If you observe carefully, the name `math` is located in the global frame. This is because our `import math` statement is outside of the function. Any code outside of any function is executed in the global frame. This is also the reason why we import the name `math` in the global frame. The reason is that more than one functions may make use of the math functions and constants. If we import the library `math` inside of each function, we have to do multiple import. + +## Variable Shadowing + +What happens if you have the same name both in the local and in the global variable? In this case, the name in the local frame takes precedence and you will not be able to access the global variable. Let's look at a simple example to illustrate this. Let's create a variable `diameter` inside the function with some specific constant value. + + + +In the above code, we have two variables with the name `chainring`. The first one is at line 8 which is outside of the function and is in the global frame while the second one is at line 12 which is inside the function and is in the local frame. When you run the code and execute line 12 (Step 11 of 18), you notice that now the gear_ratio is 0.0. The reason is that Python takes in the value of `chainring` from the local frame instead of the global frame. Notice in the environment diagram that the value of `chainring` in the local frame is 0. This is why we say that the local variable shadows the global variable when they have the same name. + +If you move the steps to the last step and observe the global frame, notice that the value of the `chainring` remains at 50. The value at the global frame does not change when we have a new definition with the same name in the local frame. If you want to modify the global frame variable's value, you can use the keyword `global` in Python inside the function definition. But modifying global variable from inside a function is creating a side effect. This will make it hard for programmers to debug as the value can be modified from any function and we will find a hard time where or how the value is modified. The best way to modify a value in the global variable is by returning the output of the function and assigning it back to the global variable. + +In summary, avoid using the global variable to modify information. If you want to pass on information to the function, pass it through the input arguments. On the other hand, if you want the function to modify some variables, modify by assigning the output of that function to that variable. In short, we want to have no side effects from a function as much as possible. Side effects make it hard for us to debug our code. On the other hand, if we use input arguments and return values, we know what data coming in and out of the functions and it will be easier to debug. + + +## Defining Functions with Optional Arguments + +The other alternative instead of using a global variable is to use optional arguments with default values. Python allows some arguments to be optional, which means that you do not need to supply the actual argument during the function call. However, to ensure that the function can still compute the output, you need to provide the default values for these optional arguments. To declare optional arguments, you just need to use the assignment operators in the function header and provide its default values. Let's use it for our `calculate_speed()` function. + +```python +import math + +Speed = tuple[float, float] + +def calculate_speed(cadence: int, + diameter: float = 685.8, + tire_size: float = 38.1, + chainring: int = 50, + cog: int = 14) -> Speed: + gear_ratio: float = chainring / cog + speed: float = math.pi * (diameter + (2 * tire_size)) \ + * gear_ratio * cadence + speed_kmph: float = speed * 60 / 1_000_000 + speed_mph: float = speed * 60 / 1.609e6 + return speed_kmph, speed_mph +``` + +In the above function, note that we provided the default values for `diameter`, `tire_size`, `chainring` and `cog`. Because Python has values for these arguments, if there are no actual arguments provided, Python will use these default values to compute. Thus, you can call this function as follows. + +```python +speed: Speed = calculate_speed(25) +``` + +In the case if any of the bicycle parameters change, you can modify the value using the argument name as the keyword. For example, if the `chainring` is 56 instead of 50, you can call the function as follows. + +```python +speed: Speed = calculate_speed(25, chainring=56) +``` + +Here we follow PEP8 guidelines of not putting space in the keyword arguments. But notice that you do not need to follow the sequence of the arguments anymore when you provide the name of the argument. Python will match the name of the arguments. This also implies that you can only put the optional arguments at the back of the compulsory arguments. Notice that we have change the sequence of the arguments by putting `cadence` to the first argument in our function definition. This is needed because for Python will supply the actual arguments based on its position in the function header. In our case, 25 is the first argument and Python will feed into the first argument in the function header, which is `cadence`. On the other hand, since the rest of the arguments are optionals, they need not be supplied with any actual arguments. You can supply only the arguments that you know is different from the default values. But now, Python does not know which optional arguments you are supplying, so in this case, you need to give the name of the argument. This why it is also called as keyword arguments. + +## Identifying Input, Output and Process of a Problem + +We have discussed how to create our own function in this section. We also say that function is one way we can abstract our computation. In our previous lessons, we mentioned that we can use PCDIT framework to solve problems, design our algorithms and implement it. In the first part of this framework, we need to state the Problem Statement, i.e. P step. We can actually apply PCDIT for every function that we design. + +In the example above, we first ask what is the input to the `calculate_speed` function? We identify a few input: +- `cadence` +- `diameter` +- `tire_size` +- `chainring` +- and `cog` + +It is also important to ask what kind of data are these inputs? We then identify: +- `cadence` to be an int +- `diameter` to be a float +- `tire_size` to be a float +- `chainring` and `cog` to be an int + +After identifying the input, we also need to identify the output of the problem and in this case is the output of the function. In our `calculate_speed()` function, the output is the speed. However, we actually output two values, one in km/h and the other one in mph. So the kind of output is a tuple of two numbers. We should expect that these two numbers are floats instead of just integers. + +The last part is to identify the computation process of the function. In our case, it is the math equation to calculate speed from the those input arguments. + +$$\text{speed} = \pi \times \left(\text{diameter} + (2 \times \text{tire\_size})\right) \times (\text{chainring}/\text{cog}) \times \text{cadence}$$ + +So we have identified: +- input +- output +- process + +This completes our Problem Statement step. This step must be done at the beginning and we can revise or revisit it again as we move on to the following steps. + +We have put this PCDIT framework discussion at the end instead of the beginning as a kind of reflection how we arrive at the final function. In actual practice, we should do our **P**roblem Statement formulation before we write down any **I**mplementation. + +Let's relook at the problem again. Instead of returning a tuple, let's state that our function only return the speed in km/h. We can then think of another computation that converts speed in km/h to speed in mph. In this new function, let's identify the input, output and the computation process. + +The input to this function is speed in km/h. We should then ask, "What kind of data is this?". We can get a hint from our `calculate_speed()` function and note that it is a float data. Similarly, the output is the speed in mph and it is a float data as well. How about the computation process then? Approximately, you can get speed in mph from km/h using by using the following: + +$$\text{speed}_{mph} = \text{speed}_{kmph} \times 0.621371192 $$ + +Now, we have our **P**roblem Statement. We have identified the input, output, and computation process. We can then translate these information into our function defnition. Since the problem is simple enough, we will skip the **C**oncrete Cases and the **D**esign of Algorithm steps. + +```python +def convert_kmph_to_mph(speed_kmph: float) -> float: + return speed_kmph * 0.621371192 +``` + +We can then test this function by putting in the actual argument in the function call. + +```python +speed_mph: float = convert_kmph_to_mph(12.82) +print(speed_mph) +``` + +You will get about 7.97 mph which is what you saw previously in the second output of `calculate_speed()`. Now, our `calculate_speed()` need not return two values but only the speed in km/h. Let's modify our function again. + +```python +import math +def calculate_speed(cadence: int, + diameter: float = 685.8, + tire_size: float = 38.1, + chainring: int = 50, + cog: int = 14) -> float: + ''' + Calculates the speed from the given bike parameters and cadence. + + Parameters: + cadence (int): Cadence in rpm. + diameter (float): Diameter of the wheel in mm. Default value = 685.8 mm. + tire_size (float): Size of tire in mm. Default value = 38.1 mm. + chainring (int): Teeth of the front gears. Default value = 50. + cog (int): Teeth of the rear gears. Default value = 14. + + Returns: + speed_kmph (float): cycling speed in km/h + ''' + gear_ratio: float = chainring / cog + speed: float = math.pi * (diameter + (2 * tire_size)) \ + * gear_ratio * cadence + speed_kmph: float = speed * 60 / 1_000_000 + return speed_kmph + +def convert_kmph_to_mph(speed_kmph: float) -> float: + ''' + Calculates the speed in mph from the speed in km/h. + + Parameters: + speed_kmph (float): speed in km/h + + Returns: + speed_mph (float): speed in mph + ''' + return speed_kmph * 0.621371192 +``` + +We can then use these two functions as follows. + +```python +speed_kmph: float = calculate_speed(25) +speed_mph: float = convert_kmph_to_mph(speed_kmph) +``` + +If we are just interested in the speed in mph, we can combine the above code into a single line of code. + +```python +speed_mph: float = convert_kmph_to_mph(calculate_speed(25)) +print(speed_mph) +``` + +// insert diagram + +Notice that Python first executes the inner function `calculate_speed(25)` and evaluates the return value. This return value is used as an input argument for `convert_kmph_to_mph()` function. See diagram above. + +You can run the file `06_speed_convert.py` with `mypy` to check if the code passes the static check. + +```sh +$ mypy 06_speed_convert.py +Success: no issues found in 1 source file +``` + +To actually run the code as a script. Execute the following. + +```sh +$ python 06_speed_convert.py +7.968731362596021 +``` + +## Summary + +In this lesson, we have learnt to create our own user defined function. We learnt how to declare a new function that takes in some input arguments and returning some output values. We also continue to use the type hinting to ensure that our code is written well and easy to debug. + +Function can be thought of as a small computation and we can abstract our computation units as separate functions. We have shown how we can solve the same problem in a few different ways. But in all these cases, we follow the PCDIT framework. We mainly use the Problem Statement and identifying the input, output as well as the computational process in this lesson. As our code becomes more complicated, we will make use of the other steps in PCDIT framework. + +Another important point is the concept of global and local variables. Local variable only exists within the context of that function when it is executed. They live only in the local frame. On the other hand, global variable is accessible from all frames. However, if there is a variable with the same name as the global variable, it will shadow the global variable. The function always access the local frame first before it searches the global frame. + +We have also introduced how we can use optional arguments in our function definition. In this case, our function can be simpler. We also show that we can chain our function call and feed the output of one function into the input of another function. \ No newline at end of file diff --git a/_Notes/Dictionary.md b/_Notes/Dictionary.md new file mode 100644 index 0000000..d5d1b70 --- /dev/null +++ b/_Notes/Dictionary.md @@ -0,0 +1,1475 @@ +--- +title: Dictionary +permalink: /notes/dictionary +key: notes-dictionary +layout: article +nav_key: Notes +sidebar: + nav: Notes +license: false +aside: + toc: true +show_edit_on_github: false +show_date: false +--- + +## Why Another Data Type? + +We started introducing data types in the early lessons of this book. We started with primitive data types such as integer, float, string and boolean values. When we discussed about string data type, we noted that this data type can be tought of as a collection of characters. We then introduced another collection data type such as tuple and list. List seems useful enough for many cases and we may wonder why we need another data type. + +Though it is true that list can be tought of as a kind of universal collection data type, there are many cases when it is inconvenient to use list. One of the best use of list is when we have a collection where the *sequence* of the items matters. This sequence is represented by the *index* of the item. This index is linear from 0 to $n-1$ where $n$ is the number of item in the list. However, there are many data where what matters is not the sequence of the items. We then need a more general data collection data type. + +Before we introduce this more general data type, it's useful to see a simple problem with the sequence in a list data. + +Previously, we represent the number of steps that our app user in a list of list. + +```python +month_steps: list[list[int]] = [[40, 50, 43, 55, 67, 56, 60], + [54, 56, 47, 62, 61, 46, 61], + [52, 56, 63, 58, 62, 66, 62], + [57, 58, 46, 71, 63, 76, 63]] +``` + +In this case, the first row represents the steps in the first week and the second row represents the steps in the second week and so on. However, we also showed that there are times, when it is more profitable to represent the data in its transposed. + +```python +month_steps: list[list[int]] = [[40, 54, 52, 57], + [51, 56, 56, 58], + [43, 47, 63, 46], + [55, 62, 58, 71], + [67, 61, 62, 63], + [56, 46, 66, 76], + [60, 61, 62, 63]] +``` + +In this second representation, the first row is for all the steps on Sundays from week 1 to week 4 and the second row is for all the steps on Mondays and so on. The problem with this list representation is that we have to *remember* that the first row is for Sundays and assumes that it is intuitive for people to know that the first day is Sunday and not Monday. But for some people, they would think that the first day is Monday rather than Sunday. The above representation does not make it easy for people to know out front whether the first row represent Sunday or Monday. + +Another example where we may need more than just a list is a codebook. Let's say your app has a feature to send encrypted message where each letter will be encrypted to another letter or emoji. Shifting the letter by the same amount is the simplest but this suffers from frequency analysis attack. Another simple way is to keep a codebook how to translate each letter to another letter. Let's say, we have the following codebook for the vocals. + +```python +codebook: list[tuple[str, str]] = [('a', 'p'), + ('e', 'f'), + ('i', 'j'), + ('o', 'c'), + ('u', 'l')] +``` + +The above codebook translates 'a' to 'p', 'e' to 'f' and so on. So if we have the word "hello", it will be translated to "hfllc". So what's the problem with this codebook representation. The problem is that to find what is the translated letter, we have to scan through the list one by one until we find the right pair that we need. This is not an issue for vocals such as the one above. But if we have a larger list where it has millions of pairings, we have to search for the right pairing before we can get the data that we want. The reason that we have to search one by one is that the pairings are stored in a list and we access the list based on its sequence. In our codebook case, the sequence is not important but the pairing is more important. Therefore, we need another data type that is more efficient than list for our purpose. This is what Dictionary data type comes into play. + +## What is a Dictionary? + +Dictionary is another collection data type that stores *key-value* pairs. In Dictionary, what matters is the pairing or the relationship between the keys and the values. The sequence of the pairs does not matter. It turns out that this data can be used for many things and is more general than list. + +Let's think for a list of numbers. Recall that we can represent the number of steps in a week in the following way. + +```python +week1_steps: list[int] = [40, 50, 43, 55, 67, 56, 60] +``` + +We can actually represent the same data using Dictionary as follows. + +```python +week1_steps: dict[int, int] = {0: 40, 1: 50, 2: 43, 3: 55, 4: 67, 5: 56, 6: 60} +``` + +In the above Dictionary, we have the keys from 0 to 6 which is the indices of the items in our previous list. At the same time, each of this key is paired with a value. The basic syntax to create a Dictionary is shown below. + +```python +var: dict[key_type, val_type] = {key_1: value_1, key_2: value_2, ...} +``` + +Notice that we use curly braces to create Dictionary literal. The key-value pairs are separated using comma as usual and marked with a colon (:). + +The great thing about Dictionary is that the key is not limited to an integer sequence. We can actually represent our data in a more intuitive way. + +```python +week1_steps: dict[int, int] = {"Sunday": 40, "Monday": 50, "Tuesday": 43, + "Wednesday": 55, "Thursday": 67, "Friday": 56, + "Saturday": 60} +``` + +There are some requirement for the data type of the *keys*. These are the requirements: +- unique +- hashable + +The keys must be unique makes much sense similar to indices of items in a list. In a list, every item has a unique index. No two items have the same index. Similarly, in a Dictionary, no two pairings can have the same keys. Every key must be unique. + +The second requirement sounds rather fancy but this is the key why Dictionary is much better for the case of our codebook example. The keys must be *hashable*. This means that Python must be able to *hash* the keys. The word *hash* is a technical term in computing. A hash is a function that converts a data into a hash value. Python has a built-in `hash()` function where you can try to hash some values. + +```python +>>> hash("Monday") +-5612247997379553688 +>>> hash("Saturday") +4116785229157965906 +``` +We will not go into detail of hash and hash function. What is important to remember is that Dictonary stores the keys as hash values and computer has a very efficient and fast way of finding them. This means that given a key, Python can compute the hash values and retrieve the data paired to this key in a very efficient way. This retrieval does not depend on the number of keys in the Dictionary unlike list. In a list, we have to go through one item in the list one by one and find the data we want. In a Dictionary, on the other hand, given a key, we can get our data immediately. + +In order for Python to hash the keys, the data type of the key must be of certain primitive data types which is called immutable. Here are some immutable data type in Python which you can use for Dictionary keys: +- int +- float +- string +- tuple + +Notice that list is not included there since list is a mutable data type. + +With this in mind, we can now represent our steps data using Dictionary. + +```python +month_steps_day: dict[str, list[int]] = {'Sunday': [40, 54, 52, 57], + 'Monday': [51, 56, 56, 58], + 'Tuesday': [43, 47, 63, 46], + 'Wednesday': [55, 62, 58, 71], + 'Thursday': [67, 61, 62, 63], + 'Friday': [56, 46, 66, 76], + 'Saturday': [60, 61, 62, 63]} +``` + +There are no requirements for the *value* of a key. It can be of any data type. Previously, we have integer as the value and now we have a list of integer as the value for each key. + +Another example where it is more appropriate to use Dictionary instead of a list is a profile data. Let's say your app has a profile data, you can group them in this way. + +```python +profile: dict = {"name": "John Wick", + "email": "john@wick.ed", + "phone": "+6591234567", + "birth-year": 1980} +``` + +Here, we see that the values of the dictionary need not even have the same type. Name, email and phone is represented as string data while birth-year is represented as integer number. The reason why birth-year is an integer is is that we can automatically compute the age if we know the birth-year of the user. + +## Basic Operations with Dictionary Data + +Now, we know what is a dictionary and how to create a Dictionary literal. In the subsequent sections, we will see how we can operate on Dictionary data. There are some common operations similar to other collection data type. + +### Getting Data from Dictionary + +To read data from a dictionary, we use the "get item" operator or the square bracket operator. This is similar to list. In a list, we put the index inside the square bracket. In the case of Dictionary, we put in the *key*. + +```python +>>> profile['name'] +'John Wick' +>>> profile['birth-year'] +1980 +``` + +Similarly, we can get the data from our month-steps dictionary. + +```python +>>> month_steps_day['Wednesday'] +[55, 62, 58, 71] +``` + +or the data from our `week1_steps`. + +```python +>>> week1_steps['Wednesday'] +55 +``` + +The key in a Dictionary is analogous to the index in the list data type. What happens if you try to access a non-existing key? The answer is an error. + +```python +>>> week1_steps['February'] +Traceback (most recent call last): + File "", line 1, in +KeyError: 'February' +``` + +The error says `KeyError` because `February` is not one of the keys in `week1_steps` dictionary. Python provides a method `get()` where we can supply a default value if the key does not exist. + +```python +>>> week1_steps.get('Wednesday') +55 +>>> week1_steps.get('February', 'Error') +'Error' +``` + +The first argument for `get()` method is the key. The second argument which is optional is the default value if the key does not exist. + +### Modifying and Adding Dictionary Data + +On top of reading the data, we can modify an existing values in Dictionary using the same get-item operator and the assignment operator. For example, we can change the name in our profile data as follows. + +```python +>>> profile['name'] = "John Nice" +>>> profile +{'name': 'John Nice', 'email': 'john@wick.ed', 'phone': '+6591234567', 'birth-year': 1980} +``` + +In the code above, we have changed the name from "John Wick" to "John Nice". + +Now, this is where Dictionary is different from List. In a list we have a few methods to add data into the list. We have `append()` to add to the end of the list. We have `insert()` to add to a particular position in a list and we have `extend()` to extend a list with another list. Appending and inserting operations make sense in list because list has a sequence. In a sequence, it makes sense to talk about the end of a list or inserting at a particular position in a list. However, dictionary has no sequence and order. We call dictionary **unordered collection** data type. This means that there is no sequence and Python does not guarantee the sequence of the key-value pairs. Because of this, Dictionary does not have any appending and inserting operation. What Dictionary has is simpply **adding a key-value pair** into ta dictionary. + +To add a key-value pair, we use the same get-item operator and assignment operator. The only thing we have to take note is that the *key* must be some value that is not present in the existing list of keys in the dictionary. For example, we can add address into our profile data. + +```python +>>> profile['address'] = "Somewhere out there." +>>> profile +{'name': 'John Nice', 'email': 'john@wick.ed', 'phone': '+6591234567', 'birth-year': 1980, 'address': 'Somewhere out there.'} +``` + +### Removing Data from Dictionary + +To remove data from a dictionary, we can use the `del` operator and the get-item operator. This is similar to delete an item from a list. In a list, we specify `del list_name[index]`. We can do the same with dictionary keeping in mind that we need to supply the key instead of the index. + +```python +>>> del profile['address'] +>>> profile +{'name': 'John Nice', 'email': 'john@wick.ed', 'phone': '+6591234567', 'birth-year': 1980} +``` + +Once we delete the key-value pair we cannot access it again. If we try to delete it again, it will give a `KeyError` error. + +```python +>>> del profile['address'] +Traceback (most recent call last): + File "", line 1, in +KeyError: 'address' +``` + +This is because `address` key has been removed in the previous command. + +### Getting the Keys and the Values + +There are times when it is useful to get only the keys or the values or the pairs of key-value in a dictionary. To do this, Python provides a few methods. To get all the keys, we can use `keys()` method of a dictionary. + +```python +>>> profile.keys() +dict_keys(['name', 'email', 'phone', 'birth-year']) +``` + +Notice that this is not a list and if we need to process this data, it is helpful to convert it into a list using the conversion function. + +```python +>>> list(profile.keys()) +['name', 'email', 'phone', 'birth-year'] +``` + +Similarly, we can get the values in a dictionary using `values()`. + +```python +>>> profile.values() +dict_values(['John Nice', 'john@wick.ed', '+6591234567', 1980]) +>>> list(profile.values()) +['John Nice', 'john@wick.ed', '+6591234567', 1980] +``` + +We can also get the key-value pairs using `items()` method. + +```python +>>> profile.items() +dict_items([('name', 'John Nice'), ('email', 'john@wick.ed'), ('phone', '+6591234567'), ('birth-year', 1980)]) +>>> list(profile.items()) +[('name', 'John Nice'), ('email', 'john@wick.ed'), ('phone', '+6591234567'), ('birth-year', 1980)] +``` + +We can see that `items()` method gives us a sequence of key-value pairs as a tuple, i.e. `(key, value)`. + +### Traversing a Dictionary + +As in any other collection data types, one of the most common operation is on how we can traverse the elements of the collections. The usual `for-in` syntax is commonly used to traverse dictionary data as well. Recall the format for `for-in` syntax in Python. + +```python +for var in iterable: + # Block A : code to repeat + do_something_here() +``` + +Since dictionary is iterable, we can actually put the dictionary name as the `iterable` in the above syntax. + +```python +for key in profile: + print(key) +``` + +The output is shown below. + +``` +name +email +phone +birth-year +``` + +Notice that the variable `key` is not a keyword but just a variable name to capture what is thrown by the iterable at every iteration. When putting the dictionary's name, Python throws out the dictionary **keys** at every iteration. We can access the value for every key-valu pair using the get-item operator as usual. + +```python +for key in profile: + print(f"key: {key}, value: {profile[key]}") +``` + +The output is shown below. + +``` +key: name, value: John Wick +key: email, value: john@wick.ed +key: phone, value: +6591234567 +key: birth-year, value: 1980 +``` + +Notice that in the code above, we get the value using `profile[key]`. However, previously, we learnt that we can actually get both key and value from a dictionary using `.items()` method. Therefore, we can write the following code to get both key and value at every iteration. + +```python +for key, value in profile.items(): + print(f"key: {key}, value: {value}") +``` + +The output is given below. + +``` +key: name, value: John Wick +key: email, value: john@wick.ed +key: phone, value: +6591234567 +key: birth-year, value: 1980 +``` + +We get the same output, but now the variable that is capturing the data at each iteration is a tuple `key, value`. Of course, there are times when we don't need the key-value pairs and maybe only need to iterate over the values. In order to do that, we can use the other methods `.values()` also mentioned in the previous section. + +```python +for value in profile.values(): + print(value) +``` + +The output is given below. + +``` +John Wick +john@wick.ed ++6591234567 +1980 +``` + +Note that we can guarantee the order of the key-value pair in dictionary. + +### Checking If an Item is in a Dictionary + +Sometimes, we don't want to traverse the dictionary by visiting every key. There are ocassions when we just want to check if the key exists in the dictionary or if a certain value is recordes somewhere in the dictionary. Let's start with the simple one first, which is to check if an item is one of the keys in the dictionary. + +Similar to list, we can use the `in` operator. When dealing with list, `in` operator check if something is an element in the list. When dealing with dictionary, the `in` operator check if something is one of the **keys** in the dictionary. + +```python +False +>>> profile: dict[str, str] = {"name": "John Wick", + "email": "john@wick.ed", + "phone": "+6591234567", + "birth-year": 1980} +>>> 'name' in profile +True +>>> 'email' in profile +True +>>> 1980 in profile +False +``` + +The last one is `False` because 1980 is the value and not the key in the dictionary `profile`. When we are traversing the dictionary, we noted that the name of the dictionary basically points to the keys. The same thing happens here. We can just use the name of the dictionary to check if something is in the keys. + +So what should we do if we want to check if something is in the value of a dictionary? We can use `.values()` to get the values of the dictionary. The nice thing is that the `in` operator works for all collection-like data type. This means that you can actually use it with the output generated by `.values()`. + +```python +>>> 1980 in profile.values() +True +``` + +This works if the values is not another collection type. If we have a nested collection type then there is no simple ways of doing this. We have to write our own function to look through the values and use `in` operator for each of the collection there. But you have learnt how to deal with nested list and nested dictionary is no different. + +## Using Dictionary to Implement Branch Structure + +Another useful use case for dictionary data is to implement branch structure. Recall the codebook example in the beginning of this section. We want to translate certain vowels into another letter. We can actually write an if-else to do this. + +```python +message: str = "hello" +encrypted: str = "" +for char in message: + if char == "a": + encrypted += "p" + elif char == "e": + encrypted += "f" + elif char == "i": + encrypted += "j" + elif char == "o": + encrypted += "c" + elif char == "u": + encrypted += "l" + else: + encrypted += char +print(f"{message} is encrypted as: {encrypted}") +``` + +In the above code, we are encrypting the message, which is `"hello"`. We replaced the vowels accordingly as specified in the beginning of this lesson. If it is not a vowel, we just add back the character into the encrypted message. The output of the above code is given below. + +``` +hello is encrypted as: hfllc +``` + +However, we can use dictionary to simply the code. We can have a codebook dictionary as follows and write the encryption as shown below. + +```python +message: str = "hello" +encrypted: str = "" +codebook: dict[str, str] = {'a': 'p', + 'e': 'f', + 'i': 'j', + 'o': 'c', + 'u': 'l'} +for char in message: + if char in codebook: + encrypted += codebook[char] + else: + encrypted += char +print(f"{message} is encrypted as: {encrypted}") +``` + +The output is shown below. + +``` +hello is encrypted as: hfllc +``` + +We have replaced the `if-else` for the translation into a single line `encrypted += codebook[char]`. We still need the if statement to check if the character is one of the vowels. If it is not, it will just put back the same character from the original message. + +One key limitation is that we can only use dictionary for branch structure where the condition is a single value. In the above example, the character is either `a` or `e` or `i` and so on. We are not able to represent a range of values. Therefore, it is hard to implement the following branch structure using dictionary. + +```python +if 10 < average_steps <= 40: + message = "Try again next month to hit your target." +elif 41 < average_steps <= 60: + message = "Well done! You hit your target." +elif 60 < average_steps : + message = "Extraordinary! You break a record." +``` + +However, for simple if-else, dictionary can make the code cleaner. This is even more so when there are many conditions. Think what would the codebook translation code look like if we also translate the consonants using if-else statement. In the case of dictionary, the code remains the same. We just need to add an extra entry into our dictionary which can be done programmatically using code. + +## Graph and Breadth First Search + +Let's look into some example where we can use dictionary and apply some problem solving framework. Let's say, our app can plan a cycling path and we would like to find the path we should take from one place to another place. This is similar to Google Maps do when finding a path from a starting location to some destination. + +The first question we may ask is how should we represent the map in our data. How should we represent the connection between one place to another place? + +One useful abstract data type is what we call a **graph**. A graph consists of vertices or nodes. These nodes are connected by edges. + +See example image below of an example map of MRT lines. What are the path we should take from Simei to Upper Changi? + + + +In the above graph, we represent every station as a vertex in the graph. Whenever two stations are connected by the MRT line, there is an edge in the graph to represent this connection. + +Similarly, we can have a map for our cycling map representing different places as shown in the image below. + + + +In our example here, we represent each place and junction as a vertex and the connection between two places or junctions as an edge. For simplicity, we will just use alphabet to represent the graph as shown below. + + + +How can we describe this graph as our data in our code? We can use a dictionary for this. In using the dictionary, we should ask ourselves what are the keys and the values. We can choose the vertices as the keys. Every graph has vertices and these vertices can be the keys in our dictionary. Moreover, each vertex can have some neighbouring vertices. These neighbours can the values for each key. We can then represent the the above graph as follows. + +```python +cycling_map: dict[str, list[str]] = {"A": ["B", "D"], + "B": ["A", "C"], + "C": ["B", "D", "F"], + "D": ["A", "C", "E"], + "E": ["D", "F"], + "F": ["C", "E"]} +``` + +Given the above data, the problem now becomes finding the path from A to F. Usually, we want to find the shortest path and the longest path. So the shortest path is assumed in this problem. We will also make another assumption to simplify our problem. We will assume that it takes the same effort to travel from one point to another. This assumption may not be true and there are ways to represent this in our dictionary. But for now, to simplify our problem, we will assume that it takes the same effort to travel from one point to another and we are only interested to find the shortest path from point A to F. + +### (P)roblem Definition + +We will start with our problem definition and try to identify the input, output and summarize the problem into a statement. + +``` +Input: + - map: dictionary[key: string, value: list of string] + - start point: string + - end point: string + +Output: + - path: list of string in the map that gives the shortest path + +Problem Statement: + Given a map, starting point and ending point in the dictionary, + the function should return a shortest path from starting point + to ending point in the map. +``` + +How should we approach this problem? Let's start working on this concrete case step by step and generalize the algorithm. + +### Concrete (C)ases + +Let's start with our input. We have three inputs. The first one is the map and it is represented by the dictionary as follows. + +```python +cycling_map: dict[str, list[str]] = {"A": ["B", "D"], + "B": ["A", "C"], + "C": ["B", "D", "F"], + "D": ["A", "C", "E"], + "E": ["D", "F"], + "F": ["C", "E"]} +``` + +The second input is our starting point. In this case, let's choose a path from A. The last input is our ending point. Let's choose the point F as our destination. We can see that we have a few paths from A to F. + +``` +path 1: A -> B -> C -> F +path 2: A -> B -> C -> D -> E -> F +path 3: A -> D -> E -> F +``` + +With our assumptions, we can either choose path 1 or path 3 as our solutions as it has shorter path or smaller number of points. How do we come to these paths? + +We start with the second input which is the starting vertex, i.e. A. What we can do now is too look into the neighbours of A. We can get A's neihbours from the dictionary. A has two neighbours, i.e. `["B", "D"]`. What we do is that we can put these to vertices into a list to visit. + +```python +to_explore: list[str] = ["B", "D"] +``` +The sequence may matter in this case. In the above, we choose to put into list alphabetically. This means that we put B first ahead of D. But we can choose otherwise. What do we do with this list? We can take out the item from the list and do the same thing as we did with A. + +This means that we take out B from the list and find the neighbours of B. Looking into our dictionary, we notice that B has two neighbours, i.e `["A", "C"]`. However, we have visited A and we don't want to go back to A. So we must keep track of those vertices we have visited. Moreover, we also don't want to explore B and D again since we have done that. So we should keep track all these vertices that we have visited. What's the strategy? Well, we can put all those we put into the `to_explore` list into another list called `visited`. We didn't really put A into the `to_explore` list. But we can do this actually. In the beginning, we can add A into the `to_explore` list and `visited` list. Then we take out A from the `to_explore` list to get its neighbours. Let's create a new list for all those vertices we have visited. By now, it should contain A, B and D. + +```python +visited: list[str] = ["A", "B", "D"] +``` + +Now, what we do is that before we add anything to the list `to_explore`, we will check if the vertex is already inside `visited` list. If the vertex is not the `visited` list, we can add that vertex to the list `to_explore`. In this case, only C will be added. + + +```python +to_explore: list[str] = ["D", "C"] +visited: list[str] = ["A", "B", "D", "C"] +``` + +Since we add all the neighbours of B, we can visit the neighbours of the next vertex in the list `to_explore`. Now, D is next. So we take out D from the list and find its neighbours. The neighbours of D are `["A", "C", "E"]`. But A and C are already in the `visited` list. So this leaves us only with E. + + +```python +to_explore: list[str] = ["C", "E"] +visited: list[str] = ["A", "B", "D", "C", "E"] +``` + +We will do the same steps. You can see that there is iteration structure here as we do the same steps again and again. We take out C from the list `to_explore` and get its neighbours, i.e. `["B", "D", "F"]`. Out of these three, B and D are in the `visited` list. Therefore, we will not add these two and only add F. But F is actually our destination and we have found our destination! + +But how do we get a path? In the above examples, our path is `A -> B -> C -> F`. How can we return a list describing the sequence points in this path? We can do that if we keep track how we visit one vertex to another and how do we get to our destination vertex. + +How can we keep track? Everytime we explore a vertex from another vertex, we can keep track which vertex is the previous vertex of the new vertex we visited. For example, when we start from A and visit B and D at the beginning, we can create a new dictionary indicating, who is the parent of B and D. The word parent here refers to the previous vertex from which B and D were visited. + +```python +parent: dict[str, str] = {"B": "A", + "D": "A"} +``` + +In the above dictionary, we take not that the parent of B is A and the parent of D is also A. Similarly, when we visit C from B, we will add this into our `parent` dictionary. + +```python +parent: dict[str, str] = {"B": "A", + "D": "A", + "C": "B"} +``` + +Next, we actually explore D to visit C. So now we have the following. + +```python +parent: dict[str, str] = {"B": "A", + "D": "A", + "C": "B", + "E": "D"} +``` + +After we explore D, we explore C. From C we visit F and found our destination. So our final dictionary is the following. + +```python +parent: dict[str, str] = {"B": "A", + "D": "A", + "C": "B", + "E": "D", + "F": "C"} +``` + +How, can then we generate the sequence of points in the path? This time, we will start with our destination F. We add this to our output path list. + +```python +path: list[str] = ["F"] +``` + +From F we find the parent from our dictionary and get C. So we add C into the list. + +```python +path: list[str] = ["C", "F"] +``` + +Notice that we have to add C to the front of F in the sequence. Then we check the parent of C and find B. We add this again to our list. + +```python +path: list[str] = ["B", "C", "F"] +``` + +Lastly, from B's parent, we get A and add this to our list. + +```python +path: list[str] = ["A", "B", "C", "F"] +``` + +We can stop now because A has no parent and A is our starting point. Now we are able to generate our output from our three inputs. Let's generalize these steps in our Design of Algorithm step. + +### (D)esign of Algorithm + +There are a lot of steps in the previous section. It is useful to reread these section again whenever you get lost in this part here. What we do as our first step is to initialize a few list and dictionary. + +``` +1. Create an empty list for *visited* and *to_explore*. + Create an empty dictionary for *parent*. +``` + +Next, we can start from our starting point and add this into our `to_explore` list. + +``` +1. Create an empty list for *visited* and *to_explore*. + Create an empty dictionary for *parent*. +2. Add start vertex to *to_explore* list. +``` + +Reading the Concrete (C)ases section again reveals that once we reach this point, we started to repeat a few steps again and again. What are the steps that we repeat? + +``` +1. Take out the next vertex to explore from the *to_explore* list. +2. Get the neighbours of this vertex. +``` + +What do we do with the neighbours of the vertex we want to explore? We do a few things. First, we add this neighbouring vertex into the dictinary to record who is the parent vertex. Second, we check if this neighbouring vertex is our destination vertex or not. If it is, then we are done. If it is not, we will add into our list to explore. But we only want to add those vertices that we have not visited. So let's write down the steps. + +Let's start by adding recording who is the parent of the current neighbouring vertex. + +``` +1. For every vertex in the neighbouring vertex, do + 1.1 Add this neighbouring vertex to the *parent* dictionary +``` + +What is important to note is that the parent of the current neighbouring vertex is the current vertex which we just take out form the *to_explore* list. + +Next, we handle the case when we found the destination vertex. + +``` +1. For every vertex in the neighbouring vertex, do + 1.1 Add this neighbouring vertex to the *parent* dictionary + 1.2 if this neighbouring vertex is our destination vertex, do + 1.2.2 Exit the search +``` +If the neighbouring vertex is not the destination vertex we will add them into the list to explore. + +``` +1. For every vertex in the neighbouring vertex, do + 1.1 Add this neghbouring vertex to the *parent* dictionary + 1.2 If this neighbouring vertex is our destination vertex, do + 1.2.2 Exit the search + 1.3 Otherwise, if the neighbouring vertex is not in the *visited* list, do + 1.3.1 Add the neighbouring vertex to the *visited* list + 1.3.2 Add the neighbouring vertex to the *to_explore* list +``` + +We can now embed the above steps into our steps after we get the neighbours of our vertex to explore. + +``` +1. Take out the next vertex to explore from the *to_explore* list. +2. Get the neighbours of this vertex. +3. For every vertex in the neighbouring vertex, do + 3.1 Add this neighbouring vertex to the *parent* dictionary + 3.2 If this neighbouring vertex is our destination vertex, do + 3.2.2 Exit the search + 3.3 Otherwise, if the neighbouring vertex is not in the *visited* list, do + 3.3.1 Add the neighbouring vertex to the *visited* list + 3.3.2 Add the neighbouring vertex to the *to_explore* list +``` + +Previously, we mentioned that we need to repeat these steps again and again. The question now is until what condition do we repeat the steps above? We can continue doing this as long as there is a vertex in the `to_explore` list. If there is no more, we can stop exploring. So let's add the iterative structure into the above steps. + +``` +1. As long as there is an item in *to_explore* list + 1.1 Take out the next vertex to explore from the *to_explore* list. + 1.2 Get the neighbours of this vertex. + 1.3 For every vertex in the neighbouring vertex, do + 1.3.1 Add this neighbouring vertex to the *parent* dictionary + 1.3.2 If this neighbouring vertex is our destination vertex, do + 1.3.2.2 Exit the search + 1.3.3 Otherwise, if the neighbouring vertex is not in the *visited* list, do + 1.3.3.1 Add the neighbouring vertex to the *visited* list + 1.3.3.2 Add the neighbouring vertex to the *to_explore* list +``` + +We can then combine these iterative structure into our overall steps. + +``` +1. Create an empty list for *visited* and *to_explore*. + Create an empty dictionary for *parent*. +2. Add start vertex to *to_explore* list. +3. As long as there is an item in *to_explore* list + 3.1 Take out the next vertex to explore from the *to_explore* list. + 3.2 Get the neighbours of this vertex. + 3.3 For every vertex in the neighbouring vertex, do + 3.3.1 Add this neighbouring vertex to the *parent* dictionary + 3.3.2 If this neighbouring vertex is our destination vertex, do + 3.3.2.2 Exit the search + 3.3.3 Otherwise, if the neighbouring vertex is not in the *visited* list, do + 3.3.3.1 Add the neighbouring vertex to the *visited* list + 3.3.3.2 Add the neighbouring vertex to the *to_explore* list +``` + +These are the steps up to the point we find the destination point. But we have not come up with the steps to get the sequence of points in the path. What we did was to `exit the search` in step 3.3.1.2 when we find the destination vertex. Let's write down the steps to get the sequence of points in the path. + +In order to do this, we need to have the `parent` dictionary. Given the `parent` dictinary, the starting point and the destination point, we can craft steps to get the sequence as described in the Concrete (C)ases section. + +We start by adding the destination vertex into our output path. + +``` +1. Add destination vertex into output list. +``` + +The next few steps are repeated again and again until we reach starting vertex. So we can write the following iterative structure. + +``` +1. Add destination vertex into output list. +2. Get the parent of the current vertex. +3. As long as the parent is not the starting vertex, do + 3.1 +``` + +What do we do in step 3? we simply make this parent vertex as our current vertex and get its parent from the dictionary again. We also add these vertices into our output path into the *first* position (refer to the Concrete Cases in case you forget why we insert into the first position). + + +``` +1. Add destination vertex into output list. +2. Get the parent of the current vertex. +3. As long as the parent is not the starting vertex, do + 3.1 Make this parent vertex as the current vertex + 3.2 Add the current vertex into the output path at the first position + 3.3 Get the parent of the current vertex from the *parent* dictionary +``` + +We keep on repeating steps 3.1 to 3.3 as long as this parent vertex is not the starting vertex. If it is, then we are done repating and simply add the last vertex, which is the starting vertex into the output path. + +``` +1. Add destination vertex into output list. +2. Get the parent of the current vertex. +3. As long as the parent is not the starting vertex, do + 3.1 Make this parent vertex as the current vertex + 3.2 Add the current vertex into the output path at the first position + 3.3 Get the parent of the current vertex from the *parent* dictionary +4. Add the parent of the current vertex into the output path at the first position +``` + +Let's implement and test these steps. + +### (I)mplement and (T)est + +As shown in the previous section, we can actually breakdown our solution into two distinct steps. The first one is to get the parent dictionary. Once we have this parent dictionary, the second step is to get the list of points in our path. So we will create three functions for this. The first one is the overall function which will call the other two functions. + +We start with the following function headers. + +```python +def create_path(start: str, + end: str, + map: dict[str, list[str]]) -> dict[str, str]: + parent_tree: dict[str, str] = {} + # our code here + return parent_tree + +def get_path(start: str, + end: str, + parent_tree: dict[str, str]) -> list[str]: + path: list[str] = [] + # our code here + return path + +def get_cycling_path(start: str, + end: str, + map: dict[str, list[str]]) -> list[str]: + parent_tree: dict[str, str] = create_path(start, end, map) + output_path: list[str] = get_path(start, end, parent_tree) + return output_path +``` + +We save the above file into `cycling_path.py`. We have also indicated the type hints for the input and output of each function. We can run mypy on this and it should return no error. + +```sh +$ mypy cycling_path.py +Success: no issues found in 1 source file +``` + +Notice that in `get_cycling_path()` function, we call the other two functions which are `create_path()` and `get_path()`. We will now work on writing test and implementation for each of these two functions. + +#### Creating the Parent Tree + +Let's start with `create_path()` function. To test it, we need to create our dictionary and provide the input. + +```python +cycling_map: dict[str, list[str]] = {"A": ["B", "D"], + "B": ["A", "C"], + "C": ["B", "D", "F"], + "D": ["A", "C", "E"], + "E": ["D", "F"], + "F": ["C", "E"]} +parent_tree: dict[str, str] = create_path("A", "F", cycling_map) +print(parent_tree) +``` + +We will now only show the code inside `create_path()` using the test code above. Running mypy and python on the file now produces an empty dictionary. + +```sh +$ mypy cycling_path.py +Success: no issues found in 1 source file +$ python cycling_path.py +{} +``` + +We can start implementing the steps to get the parent dictionary. Let's copy the steps here again for easy reference. + +``` +1. Create an empty list for *visited* and *to_explore*. + Create an empty dictionary for *parent*. +2. Add start vertex to *to_explore* list. +3. As long as there is an item in *to_explore* list + 3.1 Take out the next vertex to explore from the *to_explore* list. + 3.2 Get the neighbours of this vertex. + 3.3 For every vertex in the neighbouring vertex, do + 3.3.1 Add this neighbouring vertex to the *parent* dictionary + 3.3.2 If this neighbouring vertex is our destination vertex, do + 3.3.2.2 Exit the search + 3.3.3 Otherwise, if the neighbouring vertex is not in the *visited* list, do + 3.3.3.1 Add the neighbouring vertex to the *visited* list + 3.3.3.2 Add the neighbouring vertex to the *to_explore* list +``` + +We can start with step 1 and 2. + +```python +def create_path(start: str, + end: str, + map: dict[str, list[str]]) -> dict[str, str]: + parent_tree: dict[str, str] = {} + visited: list[str] = [] + to_explore: list[str] = [start] + return parent_tree +``` + +Note that we have combined step 1 and 2 for `to_explore` list. In the code above, instead of creating an empty list, we immediatelly add the starting vertex to `to_explore` list. Now, we can start the iterative structure of step 3. We will add the condition for the while loop. + +```python +def create_path(start: str, + end: str, + map: dict[str, list[str]]) -> dict[str, str]: + parent_tree: dict[str, str] = {} + # our code here + visited: list[str] = [] + to_explore: list[str] = [start] + while len(to_explore) != 0: + current_vertex: str = to_explore.pop(0) + print(current_vertex) + return parent_tree +``` + +Running this code produces the following output. + +```sh +A +``` + +The print out `A` is the result of `printing(current_vertex)` inside the while loop. Since there is only one vertex, the loop stops when the loop checks that the `to_explore` list is empty. The list is empty after `to_explore.pop(0)` is executed but only stops the loop when the program counter checks the loop condition. So we have done step 3.1 to get the current vertex from the list `to_explore`. Now, we need to explore and visit the neighbours of the current vertex in step 3.2 and 3.3. + +```python +def create_path(start: str, + end: str, + map: dict[str, list[str]]) -> dict[str, str]: + parent_tree: dict[str, str] = {} + # our code here + visited: list[str] = [] + to_explore: list[str] = [start] + while len(to_explore) != 0: + current_vertex: str = to_explore.pop(0) + neighbours = map[current_vertex] + print(neighbours) + return parent_tree +``` + +This time, we print out the neighbours and it will produce the following. + +```sh +['B', 'D'] +``` + +Recall that A vertex has two neighbours, i.e. B and D. Now, it is time for us to do step 3.3. We need to go through every neighbouring vertex and check. If that vertex is the destination vertex, we are done. But if it is not, we will need to add these vertices to the queue to explore their neighbours again. + +```python +for n in neighbours: + parent_tree[n] = current_vertex + if n == end: + return parent_tree + elif n not in visited: + to_explore.append(n) +``` + +We remove the rest of the code for a while so that it is easier for you to read the part when we explore the neighbours. We iterate for every vertex in the neighbours of the current vertex. We then immediately record this vertex and put the `current_vertex` as its parent. If that vertex is the destination, we stop and return the dictionary of the `parent_tree`. But before we return this dictionary, we added the the parent of the current vertex, i.e. `parent_tree[n] = current_vertex`. This means that the node `n` has `current_vertex` as its parent node. + +If `n` is not our destination vertex, we check if `n` is already in the `visited` list. If it is not, then we can add it into the `to_explore` list. Similarly, before we add, we record the parent of the current node `n`. Lastly, if `n` is already in the `visited` list, we don't need to do anything and just skip that vertex. Let's see how our create path function now works. We will add a few print statement to indicate the start and end of the iteration. + +```python +def create_path(start: str, + end: str, + map: dict[str, list[str]]) -> dict[str, str]: + parent_tree: dict[str, str] = {} + # our code here + visited: list[str] = [] + to_explore: list[str] = [start] + while len(to_explore) != 0: + print('---') + current_vertex: str = to_explore.pop(0) + print(f'Exploring {current_vertex}') + neighbours = map[current_vertex] + for n in neighbours: + print(f'neighbour: {n}') + parent_tree[n] = current_vertex + if n == end: + print('Found the end') + return parent_tree + elif n not in visited: + print(f'Adding {n} to the list of vertices to explore') + to_explore.append(n) + print(f'To explore list: {to_explore}') + print(f'Visited list: {visited}') + print(f'Parent tree: {parent_tree}') + return parent_tree +``` + +Running the script produces the following output. + +```sh +--- +Exploring A +neighbour: B +Adding B to the list of vertices to explore +To explore list: ['B'] +Visited list: [] +Parent tree: {'B': 'A'} +neighbour: D +Adding D to the list of vertices to explore +To explore list: ['B', 'D'] +Visited list: [] +Parent tree: {'B': 'A', 'D': 'A'} +--- +Exploring B +neighbour: A +Adding A to the list of vertices to explore +To explore list: ['D', 'A'] +Visited list: [] +Parent tree: {'B': 'A', 'D': 'A', 'A': 'B'} +neighbour: C +Adding C to the list of vertices to explore +To explore list: ['D', 'A', 'C'] +Visited list: [] +Parent tree: {'B': 'A', 'D': 'A', 'A': 'B', 'C': 'B'} +--- +... +``` + +We didn't show all the output because we notice something wrong. Here, we see that A is being added again to the list to explore. This not what we want. And when we see the print out of `visited` list. It is always empty. We realize that we have not added the steps to add a vertex into a visited list. When should we add a vertex into the list of visited vertices. We actually missed out step 3.3.3.1. Recall the following step. + +``` +3.3.3 Otherwise, if the neighbouring vertex is not in the *visited* list, do + 3.3.3.1 Add the neighbouring vertex to the *visited* list + 3.3.3.2 Add the neighbouring vertex to the *to_explore* list +``` + +There is another part that we have to add on and this is special for the starting vertex. The thing is that we always add to our visited list every time we add to our `to_explore` list. But 3.3.3.2 is not the only step that we add into `to_explore` list. Step 2 also adds a vertex into `to_explore` list. + +``` +2. Add start vertex to *to_explore* list. +``` + +Therefore, we need to add another step to add the starting vertex into our `visited` list as well. The revised steps are shown below. + +``` +1. Create an empty list for *visited* and *to_explore*. + Create an empty dictionary for *parent*. +2. Add start vertex to *to_explore* list and *visited* list. +3. As long as there is an item in *to_explore* list + 3.1 Take out the next vertex to explore from the *to_explore* list. + 3.2 Get the neighbours of this vertex. + 3.3 For every vertex in the neighbouring vertex, do + 3.3.1 Add this neighbouring vertex to the *parent* dictionary + 3.3.2 If this neighbouring vertex is our destination vertex, do + 3.3.2.2 Exit the search + 3.3.3 Otherwise, if the neighbouring vertex is not in the *visited* list, do + 3.3.3.1 Add the neighbouring vertex to the *visited* list + 3.3.3.2 Add the neighbouring vertex to the *to_explore* list +``` + +We did not change the numbering and only add that steps into step 2. Now, let's fix the code. + +```python +def create_path(start: str, + end: str, + map: dict[str, list[str]]) -> dict[str, str]: + parent_tree: dict[str, str] = {} + # our code here + visited: list[str] = [start] + to_explore: list[str] = [start] + while len(to_explore) != 0: + print('---') + current_vertex: str = to_explore.pop(0) + print(f'Exploring {current_vertex}') + neighbours = map[current_vertex] + for n in neighbours: + print(f'neighbour: {n}') + parent_tree[n] = current_vertex + if n == end: + print('Found the end') + return parent_tree + elif n not in visited: + print(f'Adding {n} to the list of vertices to explore') + visited.append(n) + to_explore.append(n) + print(f'To explore list: {to_explore}') + print(f'Visited list: {visited}') + print(f'Parent tree: {parent_tree}') + return parent_tree +``` + +Notice the two lines when we add the start vertex. + +```python +visited: list[str] = [start] +to_explore: list[str] = [start] +``` + +And the part that we append into the list. + +```python +elif n not in visited: + print(f'Adding {n} to the list of vertices to explore') + visited.append(n) + to_explore.append(n) +``` + +The output now is shown below. + +```sh +--- +Exploring A +neighbour: B +Adding B to the list of vertices to explore +To explore list: ['B'] +Visited list: ['A', 'B'] +Parent tree: {'B': 'A'} +neighbour: D +Adding D to the list of vertices to explore +To explore list: ['B', 'D'] +Visited list: ['A', 'B', 'D'] +Parent tree: {'B': 'A', 'D': 'A'} +--- +Exploring B +neighbour: A +To explore list: ['D'] +Visited list: ['A', 'B', 'D'] +Parent tree: {'B': 'A', 'D': 'A', 'A': 'B'} +neighbour: C +Adding C to the list of vertices to explore +To explore list: ['D', 'C'] +Visited list: ['A', 'B', 'D', 'C'] +Parent tree: {'B': 'A', 'D': 'A', 'A': 'B', 'C': 'B'} +--- +Exploring D +neighbour: A +To explore list: ['C'] +Visited list: ['A', 'B', 'D', 'C'] +Parent tree: {'B': 'A', 'D': 'A', 'A': 'D', 'C': 'B'} +neighbour: C +To explore list: ['C'] +Visited list: ['A', 'B', 'D', 'C'] +Parent tree: {'B': 'A', 'D': 'A', 'A': 'D', 'C': 'D'} +neighbour: E +Adding E to the list of vertices to explore +To explore list: ['C', 'E'] +Visited list: ['A', 'B', 'D', 'C', 'E'] +Parent tree: {'B': 'A', 'D': 'A', 'A': 'D', 'C': 'D', 'E': 'D'} +--- +Exploring C +neighbour: B +To explore list: ['E'] +Visited list: ['A', 'B', 'D', 'C', 'E'] +Parent tree: {'B': 'C', 'D': 'A', 'A': 'D', 'C': 'D', 'E': 'D'} +neighbour: D +To explore list: ['E'] +Visited list: ['A', 'B', 'D', 'C', 'E'] +Parent tree: {'B': 'C', 'D': 'C', 'A': 'D', 'C': 'D', 'E': 'D'} +neighbour: F +Found the end +{'B': 'C', 'D': 'C', 'A': 'D', 'C': 'D', 'E': 'D', 'F': 'C'} +``` + +At the end of the first iteration, we see the following. + +``` +To explore list: ['B', 'D'] +Visited list: ['A', 'B', 'D'] +Parent tree: {'B': 'A', 'D': 'A'} +``` + +We have A, B and D in our visited vertices list. On top of that, we have recorded that A is the parent for both B and D from the `parent_tree` dictionary. We have the two neighbours B and D in our next vertices to explore. + +When we explore B's neighbour, we see that A is not added into the `to_explore` list but only C vertex. The reason is that A is in our visited list. At the end of this second iteration of exploring B's neighbour, we have the following. + +``` +To explore list: ['D', 'C'] +Visited list: ['A', 'B', 'D', 'C'] +Parent tree: {'B': 'A', 'D': 'A', 'A': 'B', 'C': 'B'} +``` + +The next vertex to explore is D, which is B's sibling. We have A, B , C and D in our visited list. And we have recorded that B is the parent of C. + +Next, when we are exploring the neighbours of D, we have A, C and E. We can see that A and C were not added but only E. At the end of this iteration, we have the following. + +``` +To explore list: ['C', 'E'] +Visited list: ['A', 'B', 'D', 'C', 'E'] +Parent tree: {'B': 'A', 'D': 'A', 'A': 'D', 'C': 'D', 'E': 'D'} +``` + +Now the next node to explore is C which we take out from the list. The vertex C has B, D and F as its neighbours and F is our destination. We can see that after it sees F, it stops the iteration. + +``` +neighbour: F +Found the end +{'B': 'C', 'D': 'C', 'A': 'D', 'C': 'D', 'E': 'D', 'F': 'C'} +``` + +Now we have the `parent_tree` which we can use to trace the path from A to F. From the dictionary above, we see F has C as its parent. C has D as its parent. But now D has C as its parent. This is not right. If we look at the output above, we see that the dictionary is changed when recording who is the parent of D. + +When we are exploring A, we have recorded that D is the parent of A. + +``` +To explore list: ['B', 'D'] +Visited list: ['A', 'B', 'D'] +Parent tree: {'B': 'A', 'D': 'A'} +``` + +But along the way, we overwrite the parent of D. The reason is that for every neighbouring vertex, we always update the `parent_tree`. + +```python +for n in neighbours: + print(f'neighbour: {n}') + parent_tree[n] = current_vertex +``` + +So when we explore C and found out that D is the neighbour of C, we overwrite the parent of D to be C. See the output when we explore C below. + +```sh +Exploring C +neighbour: B +To explore list: ['E'] +Visited list: ['A', 'B', 'D', 'C', 'E'] +Parent tree: {'B': 'C', 'D': 'A', 'A': 'D', 'C': 'D', 'E': 'D'} +neighbour: D +To explore list: ['E'] +Visited list: ['A', 'B', 'D', 'C', 'E'] +Parent tree: {'B': 'C', 'D': 'C', 'A': 'D', 'C': 'D', 'E': 'D'} +neighbour: F +Found the end +{'B': 'C', 'D': 'C', 'A': 'D', 'C': 'D', 'E': 'D', 'F': 'C'} +``` + +Notice that now the parent of D becomes C instead of A. + +We should not keep on updating the parent tree dictionary. We should only update it if that vertex has no entry in the `parent_tree`. To ensure that we only update once, we can check if that node already exists in the dictionary. + +```python +if n not in parent_tree: + parent_tree[n] = current_vertex +``` + +The above code means that we only add into the parent tree if the entry does not exist in the dictionary. The code is shown below. + +```python +def create_path(start: str, + end: str, + map: dict[str, list[str]]) -> dict[str, str]: + parent_tree: dict[str, str] = {} + # our code here + visited: list[str] = [start] + to_explore: list[str] = [start] + while len(to_explore) != 0: + print('---') + current_vertex: str = to_explore.pop(0) + print(f'Exploring {current_vertex}') + neighbours = map[current_vertex] + for n in neighbours: + print(f'neighbour: {n}') + if n not in parent_tree: + parent_tree[n] = current_vertex + if n == end: + print('Found the end') + return parent_tree + elif n not in visited: + print(f'Adding {n} to the list of vertices to explore') + visited.append(n) + to_explore.append(n) + print(f'To explore list: {to_explore}') + print(f'Visited list: {visited}') + print(f'Parent tree: {parent_tree}') + return parent_tree +``` + +The output is shown here. + + +```sh +--- +Exploring A +neighbour: B +Adding B to the list of vertices to explore +To explore list: ['B'] +Visited list: ['A', 'B'] +Parent tree: {'B': 'A'} +neighbour: D +Adding D to the list of vertices to explore +To explore list: ['B', 'D'] +Visited list: ['A', 'B', 'D'] +Parent tree: {'B': 'A', 'D': 'A'} +--- +Exploring B +neighbour: A +To explore list: ['D'] +Visited list: ['A', 'B', 'D'] +Parent tree: {'B': 'A', 'D': 'A', 'A': 'B'} +neighbour: C +Adding C to the list of vertices to explore +To explore list: ['D', 'C'] +Visited list: ['A', 'B', 'D', 'C'] +Parent tree: {'B': 'A', 'D': 'A', 'A': 'B', 'C': 'B'} +--- +Exploring D +neighbour: A +To explore list: ['C'] +Visited list: ['A', 'B', 'D', 'C'] +Parent tree: {'B': 'A', 'D': 'A', 'A': 'B', 'C': 'B'} +neighbour: C +To explore list: ['C'] +Visited list: ['A', 'B', 'D', 'C'] +Parent tree: {'B': 'A', 'D': 'A', 'A': 'B', 'C': 'B'} +neighbour: E +Adding E to the list of vertices to explore +To explore list: ['C', 'E'] +Visited list: ['A', 'B', 'D', 'C', 'E'] +Parent tree: {'B': 'A', 'D': 'A', 'A': 'B', 'C': 'B', 'E': 'D'} +--- +Exploring C +neighbour: B +To explore list: ['E'] +Visited list: ['A', 'B', 'D', 'C', 'E'] +Parent tree: {'B': 'A', 'D': 'A', 'A': 'B', 'C': 'B', 'E': 'D'} +neighbour: D +To explore list: ['E'] +Visited list: ['A', 'B', 'D', 'C', 'E'] +Parent tree: {'B': 'A', 'D': 'A', 'A': 'B', 'C': 'B', 'E': 'D'} +neighbour: F +Found the end +{'B': 'A', 'D': 'A', 'A': 'B', 'C': 'B', 'E': 'D', 'F': 'C'} +``` + +Notice that now the parent of D is A and no longer C. But, you may notice one wrong entry there. We see there is this `'A': 'B'` which means that A has B as its parent. Recall that A is the starting vertex and it should not have any parent. This happens because whenever we create an entry in the parent tree dictionary, we do the following check. + +```python +if n not in parent_tree: + parent_tree[n] = current_vertex +``` + +But A is not in the parent_tree at all and so it was added when exploring the neighbours of B. To fix this, we need to think how to represent the root vertex of our tree. A root vertex is the node or the vertex at the top which has no parent at all. One way, is to represent this with an empty string. This means that instead of initializing our `parent_tree` dictionary with an empty dictionary, we can initialize it with the root vertex as shown below. + +```python +parent_tree: dict[str, str] = {start: ''} +``` + +This means that A has no parent. The final code is given below. + +```python +def create_path(start: str, + end: str, + map: dict[str, list[str]]) -> dict[str, str]: + parent_tree: dict[str, str] = {start: ''} + # our code here + visited: list[str] = [start] + to_explore: list[str] = [start] + while len(to_explore) != 0: + # print('---') + current_vertex: str = to_explore.pop(0) + # print(f'Exploring {current_vertex}') + neighbours = map[current_vertex] + for n in neighbours: + # print(f'neighbour: {n}') + if n not in parent_tree: + parent_tree[n] = current_vertex + if n == end: + # print('Found the end') + return parent_tree + elif n not in visited: + # print(f'Adding {n} to the list of vertices to explore') + visited.append(n) + to_explore.append(n) + # print(f'To explore list: {to_explore}') + # print(f'Visited list: {visited}') + # print(f'Parent tree: {parent_tree}') + return parent_tree +``` + +We have commented out all the print statement so that it can just output the return value of the function. With the correct parent tree, we can create a path from A to F. The final output of the parent tree dictionary is shown below. + +```sh +{'A': '', 'B': 'A', 'D': 'A', 'C': 'B', 'E': 'D', 'F': 'C'} +``` + +#### Implementing the Output Path + +In order to start implementing `get_path()`, we can write another test code. We can copy the output of dictionary as an input to this function. Let's write a test code to drive the implementation of `get_path()`. + +```python +parent_tree: dict[str, str] = {'A': '', 'B': 'A', 'D': 'A', 'C': 'B', 'E': 'D', 'F': 'C'} +path: list[str] = get_path('A', 'F', parent_tree) +print(path) +``` + +The revised steps, now, is shown below. + +``` +1. Create an empty list for *visited* and *to_explore*. + Create an empty dictionary for *parent*. +2. Add start vertex to *to_explore* list and *visited* list. +3. As long as there is an item in *to_explore* list + 3.1 Take out the next vertex to explore from the *to_explore* list. + 3.2 Get the neighbours of this vertex. + 3.3 For every vertex in the neighbouring vertex, do + 3.3.1 If this neighbouring vertex is not in the *parent* dictionary, then + 3.3.1.1 Add this neighbouring vertex to the *parent* dictionary + 3.3.2 If this neighbouring vertex is our destination vertex, do + 3.3.2.2 Exit the search + 3.3.3 Otherwise, if the neighbouring vertex is not in the *visited* list, do + 3.3.3.1 Add the neighbouring vertex to the *visited* list + 3.3.3.2 Add the neighbouring vertex to the *to_explore* list +``` + +We can start by the function definition for `get_path()`. + +```python +def get_path(start: str, + end: str, + parent_tree: dict[str, str]) -> list[str]: + path: list[str] = [] + # our code here + return path +``` + +Currently, it's empty and we are going to implement the steps below. + +``` +1. Add destination vertex into output list. +2. Get the parent of the current vertex. +3. As long as the parent is not the starting vertex, do + 3.1 Make this parent vertex as the current vertex + 3.2 Add the current vertex into the output path at the first position + 3.3 Get the parent of the current vertex from the *parent* dictionary +4. Add the parent of the current vertex into the output path at the first position +``` + +Let's start with step 1 and 2. + +```python +def get_path(start: str, + end: str, + parent_tree: dict[str, str]) -> list[str]: + path: list[str] = [end] + parent: str = parent_tree[end] + print(path, parent) + return path +``` + +The output is shown below. + +```sh +['F'] C +``` + +We can see that F is in the output path and F has C as its parent. Now, we iterative do steps 3.1 to 3.3 as long as the parent is not the starting vertex. + +```python +def get_path(start: str, + end: str, + parent_tree: dict[str, str]) -> list[str]: + path: list[str] = [end] + parent: str = parent_tree[end] + while parent != start: + current: str = parent + path.insert(0, current) + parent = parent_tree[current] + print(current, parent, path) + return path +``` + +We implemented steps 3.1 to 3.3 and print `current`, `parent` and the `path`. The output is shown below. + +```sh +C B ['C', 'F'] +B A ['B', 'C', 'F'] +['B', 'C', 'F'] +``` + +Notice that in the first iteration, the current node is C and we added C into the path to get `['C', 'F']`. The parent of C is B and so the next line shows that the current node is B and we added B into the output path to get `['B', 'C', 'F']`. Now the parent of B is A. Since A is the starting vertex, we stop the iteration. So the last line prints out the current output path, but it's missing the starting vertex. So we need to add step 4 which is to add the starting node into the output path. + +```python +def get_path(start: str, + end: str, + parent_tree: dict[str, str]) -> list[str]: + path: list[str] = [end] + parent:str = parent_tree[end] + while parent != start: + current:str = parent + path.insert(0, current) + parent = parent_tree[current] + path.insert(0, start) + return path +``` + +The output is shown below. + +```sh +['A', 'B', 'C', 'F'] +``` + +#### Final Function + +Now we found the path from A to F. We can combine this into one single function and test it. Let's start with the test code first. + +```python +output: list[str] = get_cycling_path('A', 'F', cycling_map) +print(output) +``` + +We can compose the two functions to create a solution for `get_cycling_path()` as shown below. + +```python +def get_cycling_path(start: str, + end: str, + map: dict[str, list[str]]) -> list[str]: + parent_tree: dict[str, str] = create_path(start, end, map) + output_path: list[str] = get_path(start, end, parent_tree) + return output_path +``` + +Running mypy and python gives you the following output. + +```sh +$ mypy cycling_path.py +Success: no issues found in 1 source file +$ python cycling_path.py +['A', 'B', 'C', 'F'] +``` + +## Summary + +In this section, we introduced a new data type called dictionary. We compared this data type with list and showed how dictionary can be seen as a more general collection data type. Dictionary contains key-value pairs. Some of the operations that we see in other collection data type can also be done in dictionary. One crucial difference is that dictionary is not-ordered and there is no sequence in the collection. One of the key operation is how to traverse a dictionary either its keys or its values. Lastly, we showed a big example how dictionary can be used in a real application. We used it to represent a map and write a code to find a path from one point to another. The map was represented as a graph which is an abstract data type useful to represent many things not only maps. We applied PCDIT to craft the solution to find a path in a graph. We created not only one function but break it down into several functions and use composition to get the final solution. \ No newline at end of file diff --git a/_Notes/Divide_Conquer.md b/_Notes/Divide_Conquer.md deleted file mode 100644 index 78cf643..0000000 --- a/_Notes/Divide_Conquer.md +++ /dev/null @@ -1,355 +0,0 @@ ---- -title: Divide and Conquer -permalink: /notes/divide_conquer -key: notes-divide-conquer -layout: article -nav_key: Notes -sidebar: - nav: Notes -license: false -aside: - toc: true -show_edit_on_github: false -show_date: false ---- - -Many computing problems are recursive in structure. With the word *recursive* we mean that to solve a given problem, they *call themselves* one or more times to solve a closely related problems. These algorithms in a way follow a *divide-and-conquer* approach. - -Divide and conquer approach breaks the problem into several subproblems that are similar to the original problem but smaller in size. The algorithm then solves this smaller problems and then combines the solutions to create the solution to the original problem. The divide-and-conquer paradigm involves three steps at each level of the recursion: -1. *Divide* the problem into a number of smaller problems. -1. *Conquer* the smaller problems by solving them recursively and when the problem is small enough, hopefully, it can be solved trivially. -1. *Combine* the solutions of the smaller problems into the solution for the original problem. - -## Summing The Elements of an Array - -We will start giving example of divide-and-conquer approach by solving the following problem: summing a list of numbers in an array. Given a list of array, we can sum them by iterating over each element and sum each element to get the final solution. This *iterative* solution looks as follows. - -```python -def sum(array): - result = 0 - for number in array: - result += number - return result - -input_array = [4, 3, 2, 1, 7] -print(sum(input_array)) -``` - -The output is -```sh -17 -``` - -### (C)ases - -Another way of solving this problem can be done using a *divide-and-conquer* approach. In this approach we divide the input array into two parts. The first part consists of a single number while the second part consists of the rest of the numbers. For example, using the input in the above example, we have - -``` -[4, 3, 2, 1, 7] -``` - -The above input is now divided into two parts: - -``` -4 | [3, 2, 1, 7] -``` - -This is the *divide* step. In the *conquer* step, we recursively solve the second part using the same method. This means that we divide the second part into two parts as follows. - -``` -[3, 2, 1, 7] -``` -becomes - -``` -3 | [2, 1, 7] -``` -And this continues recursively - -``` -[2, 1, 7] -``` -becomes - -``` -2 | [1, 7] -``` - -and - -``` -[1, 7] -``` -becomes - -``` -1 | [7] -``` - -Now the second part consists of only a single number, i.e. 7. Since the problem is small enough, the solution to the sum problem is simply this number, which is 7. This means that the sum of an array with a single number is just that number. So we can now proceed to the *combine* step. We will combine the result by adding the number on the left with the sum of the array on the right. - -``` -1 | [7] = 1 + 7 = 8 -``` -and now we can move up -``` -2 | [1, 7] = 2 + 8 = 10 -``` -similarly, we have these two summations -``` -3 | [2, 1, 7] = 3 + 10 = 13 - -4 | [3, 2, 1, 7] = 4 + 13 = 17 -``` - -### (D)esign of Algorithm - -Let's write down the steps on how we solve those particular cases. - -``` -Input: Array or list of numbers -Output: the Sum of the array -Steps: -1. if the number of element is one only - 1.1 Return that element as the sum of the array -2. Otherwise, - 2.1 Return the addition of the first element with the sum of the rest of the array -``` - -### Recursive Basic Structure - -In all recursive solutions, we can always identify two **cases**: - -1. **Base** Case -1. **Recursive** Case - -In the algorithm steps above, step 1 is the **base** case. Base case is usually trivial and easy to solve. Recall that the strategy for this way of divide and conquer is to divide the problem into smaller problems up to the point that the problem becomes trivial. In this example, a trivial problem is to sum an array with only one element. In this case, the sum is just that element. - -Step 2 is the **recursive** case. Notice in step 2.1 we are adding the first element with the *sum of the rest of the array*. The *sum of the rest of the array* is the **same** problem but with one element less. In this way, we have made the problem smaller. It is called *recursive* because this step calls the function itself with a smaller number of element in the array. How to break down and make the problem smaller will be one of the challenge in creating recursive solutions. Moreover, identifying the base case will also be important. Let's take a look at some more example in a later section. But for now, we will analyze the computation time. - -## Computation Time - -As you can guess from the *iterative* solution, this problem takes $O(n)$ time. This means that as the number of input increases, the computation time increases linearly. How do we come about this computation time for recursive solution? One way to do this is to draw the *recursion tree*. - -In a recursion tree, each node represents the cost of a single subproblem somewhere in the set of recursive function invocations. We then have to do two summations. The first sum is to sum all the cost on each level of the tree to obtain a set of cost-per-level. The second sum is to sum all cost-per-level over all levels to obtain the total cost. - -To illustrate that, let's take a look at the example above on summing the elements in an array. Looking at the pseudocode, we see that the program will execute either step 1 or step 2 and not both. We can then make the following observation: -* if it is the base case, the computation time is $O(1)$ because it takes constant time to check the case and constant time to return the element of an array. -* if it is the recursive case, the computation time is $T(n) = O(1) + T(n-1)$. The constant time comes from the addition operation which is the *combine* step. On the other hand, this computation time contains $T(n-1)$ due to the recursive call for $n-1$ elements array. - -The recursion tree can be drawn as follows. - -![](/assets/images/week3/sum_recursiontree.png) - -The first figure on the left shows that the computation time for $n$ elements is a constant $c$ plus $T(n-1)$. The second figure in the middle shows the tree when we expland $T(n-1)$. That recursive call is when the input array is $n-1$ elements. The computation time when $n-1$ can be explanded as another $c$ plus $T(n-2)$. We can continue doing this until we are left only with one element, which is shown on the figure on the right with $T(1)$. The tree only has one child on each node and each level has the same cost which shown by the arrow pointing to the right. We can show that there are $n$ levels in the three for $n$ elements of input in the original call. - -The cost for each level is a constant $c$ and we have $n$ levels, so the total cost is $cn$, and therefore we can say that the computation time for this recursion is $T(n) = O(n)$. - -## Factorial Problem - -Factorial problem can be defined recursively as follows: - -### (P)roblem Definition - -Taking an integer input $n$, the factorial can be calculated as follows: - -$$n! = n \times (n-1)!$$ - -We also define the factorial of the following input numbers: -$$0! = 1$$ -$$1! = 1$$ - -Let's take a look at some particular examples. - -### (C)ases - -Besides those two inputs 0 and 1, we can calculate, for example, the factorial of 5 as follows: - -$$5! = 5 \times 4!$$ - -And we calculate 4 factorial with - -$$4! = 4 \times 3!$$ - -Similarly, - -$$3! = 3 \times 2!$$ - -and - -$$2! = 2 \times 1!$$ - -but we have defined that $1! = 1$. This is the base case. So we can calculate back up. - -$$2! = 2 \times 1 = 2$$ - -$$3! = 3 \times 2! = 3 \times 2 = 6$$ - -$$4! = 4 \times 3! = 4 \times 6 = 24 $$ - -$$5! = 5 \times 4! = 5 \times 24 = 120 $$ - -Now we can write the steps. - -### (D)esign of Algorithm - -``` -Input: n, an integer -Output: factorial of n, an integer -Steps: -1. if n is equal to 0 or to 1 - 1.1 return 1 -2. otherwise, - 2.1 return n x factorial of n-1 -``` - -Notice again here that step 1 is the **base** case which is trivial. The base case is also the **terminating** case. Without the base case the recursive solution will not end. So it is important to remember that every recursive solution must have a base case. - -Step 2 is the **recursive** case. In this recursive case, we divide the problem of calculating factorial of $n$ into a smaller problem to calculate only the factorial of $n-1$ and a multiplication. Calculating the *factorial* of $n-1$ calls the function itself because it is exactly the same problem but with a smaller integer at the input. Let's discuss the computational time. - -### Computational Time - -The computation time can be calculated as follows: - -* To check whether it is the base case or not, it takes constant time $O(1)$. -* If it is the base case, it returns and exits the function immediately. So the computation time when it is a base case is simply constant time, i.e. $O(1)$. -* On the other hand, if it is not a base case, the computation time is $T(n) = O(1) + T(n-1)$. The constant time is the time it takes to multiply $n$ with the factorial of $n-1$. The second term $T(n-1)$ is the computation time to calculate the factorial of $n-1$. - -From the above describe, we have the same recursive tree as before. - -![](/assets/images/week3/sum_recursiontree.png) - -From here, we can conclude that the computation time is linear, i.e. $T(n) = O(n)$, a function of the input integer. - -In the above two examples, summing an array and calculating a factorial, the solutions can be written either using recursion or iteration (such as a for-loop). It is not evident why we need a recursive solution in these two cases. However, there are cases when the iterative solution would be too complicated and its recursive solution is simple and elegent. One example of this is the problem of Tower of Hanoi which we will discuss next. - -## Tower of Hanoi Problem - -Tower of Hanoi is a classic example of when its recursive solution is simple and elegant. The problem is specified as follows. - -### (P)roblem Definition - -Given $n$ disks and three towers, one has to move the $n$ disks from the *source* tower to the *destination tower using the other *auxilary* tower. See figure below. - -![](/assets/images/week3/tower_of_hanoi.jpeg) - -In the above figure, we have three towers A, B, and C. And we have to move $n=3$ disks from tower A to tower B using tower C as the auxiliary tower. - -The rules in moving the disks are as follows: - -* We can only move one disk at a time -* A bigger disk cannot be placed on top of a smaller disk - -Note that $n$ can be 1 or greater. - -### (C)ases - -Let's look at some particular cases. Let's start with a simple case when $n=1$. In this case, to move one disk from A to B is trivial. - -1. Move disk 1 from A to B - -and we are done. - -How about when $n = 2$. Recall that our source tower is A and the destination tower is B while the auxiliary tower is C. In order to move two disks, we can do the following. - -**Moving two disks:** - -1. Move disk 1 from A (source) to C (auxiliary) -1. Move disk 2 from A (source) to B (destination) -1. Move disk 1 from C (auxiliary) to B (destination) - -Let's take a look when $n = 3$. To work this out, you can use the [simulation for tower of Hanoi](https://www.mathsisfun.com/games/towerofhanoi.html). We will divide this into a few step. The first step is to move the first two disks from A (source) to C (auxiliary). In order to move two disks, we already have the solution as shown above. So we can use the steps above with the difference in the destination and auxiliary towers. - -**Step 1** - -1. Move disk 1 from A (source) to B (destination) -1. Move disk 2 from A (source) to C (auxiliary) -1. Move disk 1 from B (destination) to C (auxiliary) - -Now we have the first two disks at C (auxiliary). The next step is to move disk 3 from A (source) to B (destination). This consists of only one step. - -**Step 2** - -1. Move disk 3 from A (source) to B (destination) - -The last step is to move the two disks from C (auxiliary) to the destination tower B. This again involves a similar three steps with differences in the source and destination towers. The steps to move the two disks to the final destination tower is as follows. - -**Step 3** - -1. Move disk 1 from C (auxiliary) to A (source) -1. Move disk 2 from C (auxiliary) to B (destination) -1. Move disk 1 from A (source) to B (destination) - -Notice that **Step 1** and **Step 3** involve the same steps as in **Moving two disks**. In fact, we can see Step 1 as Moving two disks with tower A as the source and tower C as the destination using tower B as the auxiliary tower. Similarly, we can see Step 3 as moving two disks with tower C as the source and tower B as the destination with tower A as the auxiliary tower. Moreover, the steps in **Moving two disks** are exactly the same **three steps**. We first move the top disks to the auxiliary tower, then the bottom disk to the destination tower, and lastly the top disks to the destination tower. In this way, we can use the same steps whenever there are more than 1 disks. In general, we can view this problem for $n$ disks as shown in the figure below. - -![](/assets/images/week3/tower_of_hanoi_general.jpeg) - -where we divide the disks into two parts: - -- the first $n-1$ top disks -- the last disk $n$ - -We can then generalize the Tower of Hanoi problem to any number of disks as in the following steps. - -### (D)esign of Algorithm - -``` -Input: - - n, number of disks - - source tower - - destination tower - - auxiliary tower -Output: - - sequence of steps to move n disks from source to destination tower using auxiliary tower -Steps: -1. if n is 1 disk: - 1.1 Move the one disk from source to destination tower -2. otherwise, if n is greater than 1: - 2.1 Move the first n - 1 disks from source to auxiliary tower - 2.2 Move the last disk n from source to destination tower - 2.3 Move the first n - 1 disks from the auxiliary tower to the destination tower -``` - -Notice that in the above steps, step 1 is the **base** case while step 2 is the **recursive** case. The base case is when the solution is trivial. This is the case when there is only one disk. The solution involves only one step which is to move that one disk from the source to the destination tower. On the other hand, step 2 is the recursive case. In this step, both steps 2.1 and step 2.3 are the recursive steps that reduce the problem smaller to $n-1$ disks. Step 2.2 consists of only a single step. - -Looking into these steps and compare it with the examples in the previous steps, we observe that both the steps in **moving two disks** and the three steps in moving $n=3$ disks fall under the same recursive step 2. Both consist of three general steps 2.1, 2.2, and 2.3. - -### Computational Time - -Now, let's analyze it's computational time. Looking into the steps above, we note the following: - -* it takes $O(1)$ to do the comparison whether it is one disk or more -* if it is only one disk, it takes $O(1)$ time to produce the single step and exit the function -* otherwise, it takes $T(n-1)$ time for step 2.1, and $O(1)$ for step 2.2, and another $T(n-1)$ for step 2.3 - -Let's draw the recursive tree for 3 disks. - -![](/assets/images/week3/tower_of_hanoi_time_3.jpeg) - -Note that if $n=3$, it takes constant $c$ time and $2 \times T(n-1) = 2 \times T(2)$ computational time. In the recursive steps, each of this $T(2)$ recursive call consists of another constant $c$ time and $2 \times T(1)$ time. The tree stops at $i = n - 1 = 2$ when it reaches the base case as there is only one disk left. - -Now we can generalize this to $n$ disks. The recursive tree looks as the one below. - -![](https://www.dropbox.com/s/lm7p0w6lgq89yge/tower_of_hanoi_time_n.png?raw=1) - -There will be $n$ levels from $i = 0 $ up to $i = n - 1$. Moreover, at each level, the sum total time is $2^i \times c$. If we sum up all the levels, we have the following series: - -$$T(n) = \sum_{i = 0}^{n-1} 2^i c = c \sum_{i = 0}^{n-1}2^i$$ - -Recall that the sum for a Geometric series is given as follows - -$$a + ar + a r^2 + \ldots + a r^{n-1} = \frac{a(1-r^n)}{1 - r}$$ - -where $a$ is the constant of the series and $r$ is the ratio. In our case, $a = c$ and $r = 2$. So we have - -$$T(n) = c \sum_{i = 0}^{n-1}2^i = \frac{c(1-2^n)}{1 - 2} = c (2^n -1) = O(2^n)$$ - -This means that the computational time for the Tower of Hanoi problem is *exponential* with respect the input. As the number of input increases, the time it takes increases exponentially. - -* when $n = 1$, it takes 1 step -* when $n = 2$, it takes 3 steps -* when $n = 3$, it takes 7 steps -* when $n = 4$, it takes 15 steps -* when $n = 5$, it takes 31 steps - -Notice also that in this case $c = 1$ as we can get the number of steps from $2^n - 1$. \ No newline at end of file diff --git a/_Notes/For_Loop.md b/_Notes/For_Loop.md new file mode 100644 index 0000000..ac53254 --- /dev/null +++ b/_Notes/For_Loop.md @@ -0,0 +1,546 @@ +--- +title: For Loop +permalink: /notes/for-loop +key: notes-for-loop +layout: article +nav_key: Notes +sidebar: + nav: Notes +license: false +aside: + toc: true +show_edit_on_github: false +show_date: false +--- + +## Implementing Iterative Structure Using For Loop + +In the previous section, we have discussed in detail how to implement the branch structure. The last basic control flow stucture is the iterative structure. In this lesson and the next, we will show you how to implement iterative structure using Python. + +Iterative structure is common when you deal with collection-like data type. We will deal with more collection-like data type in future lessons. However, for now, we can explore iterative structure using `string` data. Recall that string can be thought of like a collection of characters in some sequence. We can process each character using iterative structure. Similar processing can be done when we deal with other collection-like data such as list and dictionary. + +So how do we implement the iterative structure using Python? Python has two ways of implementing iterative structure. The first one is using **for-in** statement and the second one using **while-loop** statement. In this lesson, we will explore using the **for-in** statement. + +The general syntax for the for-in statement is as follows. + +```python +for element in iterable: + # code block A + # which will be repeated for every element in iterable +``` + +The right hand side of the `in` operator in the for-in statement must be an **iterable**. An iterable is any Python objects that can be iterated. This is basically saying that it must be collection-like data type where you can iterate over its element. String data is one of them. All these iterables supports the method `__iter__()` which will return the next element in the collection at every iteration. That next element is assigned to the `element` variable which is the left hand side of the `in` operator in the for-in statement. + +Therefore in a for-in statement, we iterate or repeate the code block A as many as the number of items in the iterable. If there are 11 items in the iterable, the code block A will be repeated 11 times. Each time, Python will assign the element of the iterable to the variable `element` which we can use to process the element. If we don't need the `element` in our code block A, we can choose to ignore it as follows. + +```python +for _ in iterable: + # code block A + # which will be repeated for the number of items in iterable +``` + + + +Let's take a look at some examples how to use that syntax. Let's say, we want to print every character in a name. We can write the following code. + + +```python +name: str = "John Wick" +for char in name: + print(char) +``` +In the above code, the `iterable` is the variable `name` which is a string. Remember that `string` is one of the iterables in Python. At every iteration, an element in the string will be assigned to `char`. + +The output of that code is shown below using Python Tutor. + + + +In the above code, the instruction that is repeated for every element (code block A) is the `print(char)` statement. In this case, we are instructing Python to print the element of the iterable. + +One common mistakes that many novice programmers tend to do is processing the collection as a whole when what they wanted is processing the element. An example of this logic error is printing the whole name repetitively instead of printing the characters. + +```python +name: str = "John Wick" +for char in name: + print(name) +``` + +In this code, what we print is the string and not the characters. See the output below. + + + +Notice that instead of printing each character on every line, what the code does is printing the whole string on every line. + +Sometimes, you just want to repeat based on the number of element in the collection. In that case, you don't really use the element in the collection. As mentioned previously, you can ignore the element using an underscore, i.e. `_`. The code below print asterisks as many as the number of characters in the password. + +```python +password: str = "D0n't us3 simpl3 passw0rd#" +for _ in password: + print('*', end='') +``` + +In the above code we use the option `end=''` to replace the default new line character that is always added by the print statement. By default the ending character of a line is a new line character, i.e. `\n`. This is why the next print statement is displayed in the line below. By changing the ending character to an empty string, we can display the next string in the same line. See the output using Python Tutor. + + + +## Enumerating a Collection + +There are times when it is useful to work with both the element and the index of a collection-like data. For example, we can get both the character and its index from a string. To do this, Python provides `enumerate()` function that returns a tuple of `(index, element)`. + +```python +name: str = "John Wick" +for (idx, char) in enumerate(name): + print(f"Character {char} is at position: {idx + 1}) +``` + + + +We can see that the `enumerate()` function returns an iterable. Each element of this iterable contains two things. The first one is the index of that element and the second one is the element itself. In our code block A which is repeated, we make use of both the index and the element to print both the position, which is `idx + 1` and the character itself. + + +## Range Function and Iteration Using Index + +Previously, we mentioned that the right hand side of the `in` operator in `for-in` syntax must be an *iterable*. One simple way to create an iterable that can be used for indexing the elements in a collection data type is `range()` function. This function `range()` produces an iterable which element is a sequence of integers. + +```python +>>> range(10) +range(0, 10) +>>> for item in range(10): +... print(item) +... +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +``` + +In the above code, `range(10)` produces a range of values from `0` to `9`. Notice that it does not include `10`. This similar convention can be found when we slice a string in the previous lesson. In slicing, the ending index is excluded. Similarly with `range()` function. By default the starting index is 0, but you can modify it following this syntax below. + +```python +range(start, end, step) +``` + +For example, you can have something like the following. + +```python +>>> for item in range(10,100,10): +... print(item) +... +10 +20 +30 +40 +50 +60 +70 +80 +90 +``` + +Notice again that `100` is excluded. In that range function, we put our starting number to be 10 and a step of 10. + +Range function is useful when we need an index of each character or element of a collection data type. We can rewrite our previous code where we used `enumerate()` in a different way using `range()` function. See below. + +```python +name: str = "John Wick" +for idx in range(len(name)): + char: str = name[idx] + print(f"Character {char} is at position: {idx + 1}") +``` + +You can check that the output the same by running it in Python Tutor. + + + + +## Using Print to Debug For Loop + +Iterative structure can be complicated, especially when you have a number of computation inside the body of the loop (code block A) which is repeated. The reason is that you need to keep track at which iteration you are at and what is the state of the variables at that iteration. In this section, we will share little tips on how to keep track iterative structure using table and `print()` statement. + +Let's start with our last code. + +```python +name: str = "John Wick" +for idx in range(len(name)): + char: str = name[idx] + print(f"Character {char} is at position: {idx + 1}") +``` + +First, we can note that `range(len(name))` will be evaluated from the inside to the outside as what we have learned previously in [Calling a Function]({ "/notes/calling-function" | relative_url }). This means that `len(name)` will be evaluated to `9` and then Python will evaluate `range(9)`. This function calls produces a range from 0 to 8. These values will be put into the variable `idx` at every iteration. + +It is useful then, to use print statement to print `idx` values at each iteration. + +```python +name: str = "John Wick" +for idx in range(len(name)): + print(idx) +``` + +We can setup a table to keep track the variable state at each iteration as follows. +1. Evaluate the iterable and identify the variable that is iterated at each iteration. We have done this step just now where we identify `idx` to be a range of numbers from 0 to 8. +1. Write a table where the left column is the iterated values. +1. Write the different column to be all the expression we want to keep track. +For example, in the above code, we can construct the following table. + +| idx | name[idx] | char | idx + 1 | print output | +|-----|-----------|------|---------|--------------| +| 0 | | | | | +| 1 | | | | | +| 2 | | | | | +| 3 | | | | | +| 4 | | | | | +| 5 | | | | | +| 6 | | | | | +| 7 | | | | | +| 8 | | | | | + +What we have put in the columns are the various expression in the body of the loop. Looking at the code below, we identified three others besides `idx`: +- `name[idx]` +- `char` +- `idx + 1` + +```python +name: str = "John Wick" +for idx in range(len(name)): + char: str = name[idx] + print(f"Character {char} is at position: {idx + 1}") +``` + +Notice that the table is similar to the output of the code when it is at the following state. + +```python +name: str = "John Wick" +for idx in range(len(name)): + print(idx) +``` + +We can use both paper and `print()` statement to debug our code when we implement our solutions. Recall that **T**esting should be done together with the **I**mplementation. Printing the elements of the iterable is the first thing we always advise novice programmers to do. This is to ensure that we know what is the elements being iterated. + +We also put into the tables a few other values to keep track such as `name[id]`, `char` and `idx + 1`. We start from the first row and fill up the table. Recall that `name` is assigned to `John Wick`. + +| idx | name[idx] | char | idx + 1 | print output | +|-----|-----------|------|---------|-------------------------------| +| 0 | J | J | 1 | Character J is at position: 1 | +| 1 | o | o | 2 | Character o is at position: 2 | +| 2 | h | h | 3 | Character h is at position: 3 | +| 3 | n | n | 4 | Character n is at position: 4 | +| 4 | | | 5 | Character is at position: 5 | +| 5 | W | W | 6 | Character W is at position: 6 | +| 6 | i | i | 7 | Character i is at position: 7 | +| 7 | c | c | 8 | Character c is at position: 8 | +| 8 | k | k | 9 | Character k is at position: 9 | + +We can also use print statements to verify the expected output by displaying the expressions in the columns of the above table. + +## Identifying Iterative Structure in a Problem + +Iterative structure is easy to identify. Whenever you have some steps or calculation that you repeat, there you have the iterative structure. In our **D**esign of algorithm step, we can write a few drafts of the algorithm from a very raw into some pseudocode that is closer to programming language. In these earlier drafts we can try to spot if there is any steps that are being repeated. + +Let's see this by looking at an example. Given a message, we want to encrypt the message using a fibonacci sequence. Let's say we have a message as follows. + +```python +message: str = """Let us cycle tomorrow at 4pm starting from City Hall""" +``` + +We would like, for some reason, to encrypt that message using a [Caesar Cipher](https://www.cryptomuseum.com/crypto/caesar/cipher.htm). The encryption and decryption is simple. Each letter of the plaintext is shifted down by 3 position. Here is a table of characters and its encrypted letters after shift. + +| A | X | +|---|---| +| B | Y | +| C | Z | +| **D** | **A** | +| E | B | +| F | C | +| G | D | +| H | E | +| I | F | +| J | G | +| K | H | +| L | I | +| M | J | +| N | K | +| O | L | +| P | M | +| Q | N | +| R | O | +| S | P | +| T | Q | +| U | R | +| V | S | +| W | T | +| X | U | +| Y | V | +| Z | W | + +Let us define our **P**roblem Statement. +- Input: A message, data type is string. +- Output: An encrypted message, data type is string. +- Process: Encrypt the input message using Caesar Cipher to produce the encrypted output message. + +We can then work on our **C**oncrete Cases. Let's take in the input message as given above. + +``` +input: Let us cycle tomorrow at 4pm starting from City Hall +``` + +We are expecting an output of something like the following below. + +``` + +``` + +The question we are interested in **C**oncrete Cases is how do we actually do it step by step computationally. To simplify our case, we will convert our message to all capital letters. That will be our first step. + +``` +input: "LET US CYCLE TOMORROW AT 4PM STARTING FROM CITY HALL" +``` + +After this, we will go through every letter from left to right and use the table to get the encrypted message. + +The first letter is `L`. This will be changed to the letter `I`. Then, we go to the second letter and get `E`. This is changed to the letter `B`. Then, we go to the third letter to get `T`. This is changed to the letter `Q`. And we can repeat these steps until the last letter `L` which will be changed to `I`. + +Notice, that we have spot our iterative structure here. In the previous paragraph, we mentioned that we will repeate the steps until the last character. Whenever we spot some steps that is to be repeated, we find an iterative structure. + +| iteration | message | encrypted | +|-----------|---------|-----------| +| 1 | L | I | +| 2 | E | B | +| 3 | T | Q | +| 4 | | | +| 5 | U | R | +| 6 | S | P | +| 7 | | | +| ... | ... | ... | +| 52 | L | I | + +Now we can write our **D**esign of Algorithm. +1. Convert the input message to all capital letters. +2. Take the first character +3. Shift down the letter by 3 +4. Take the second character +5. Shift down the letter by 3 +6. Take the third character +7. Shift down the letter by 3 +8. repeat the two steps above until the last character +9. Combine all the encrypted characters into one single string. + +Here, we can identify the repetition and its iterative structure. Steps 3 to 4 is repeated from the first character to the last character. Another thing that we want to modify is step number 9. From our lesson on String, we have learnt that in Python, string data type is immutable. It means that we cannot change a string once it is created. However, we can concatenate the string. String concatenation is one of the common operations performed on string data. With this in mind, we can revise our **D**esign of Algorithm to the following. + +``` +input: message -> string data type +output: encrypted message -> string data type +1. Create an empty string for output, name it as *result*. +2. Convert *message* to all capital letters, name it as *message_in_caps*. +3. For each character in *message_in_caps* + 3.1 Get the character + 3.2 Shift down the character by three letters + 3.3 Concatenate the shifted letter to *result* +``` + +We have revised our algorithm one time. We can refine it again by looking at how we want to implement it. For example, step 1 can be done using the `upper()` method of string objects. However, how should we do step 3.2? One way to do this is to convert the character to an ASCII number and use subtraction and convert back the number to character. We have `ord()` function to change a character to its ASCII number and `chr()` to change a number to its character. + +``` +input: message -> string data type +output: encrypted message -> string data type +1. Create an empty string for output, name it as *result*. +2. Convert *message* to all capital letters, name it as *message_in_caps*. +3. For each character in *message_in_caps* + 3.1 Get the character + 3.2 Change the character to ASCII number + 3.3 Subtract the ASCII number by 3 + 3.4 Change the number back to character, and + 3.5 Concatenate the character to *result* +``` + +I hope you notice that PCDIT framework is not linear. We go back and forth even between **D**esign of Algorithm and **I**mplementation. We need to know some implementation in the programming language that we choose in order to refine our **D**esign of Algorithm. But more importantly, we also refine our **D**esign of Algorithm by identifying its repetitive structure. Now, we can do the **I**mplementation and **T** at the same time. We will write the code step by step using `print()` function in between to test the expected output. + +Let's start with a simple test code and a function definition. + +```python +def encrypt(message: str) -> str: + result: str = "" + # your code here + return result + +input_message: str = "Let us cycle tomorrow at 4pm starting from City Hall" +output: str = encrypt(input_message) +print(output) +``` + +In the function definition, we specify that our input argument is a string and this function will return the encrypted string. On top of that, we have done step 1 which is to create an empty string to store our output result. + +The first thing we may want to do is simply to print the input arguments and see what kind of value and data type it is. + + + +If you run the code to the end, you will see the input message displayed and `None`. The output `None` comes from the `print(output)`. The reason is that our function does not return anything and so by default Python returns a `None` object. This is the one displayed by `print(output)`. + +Now, let us do step 1. + +```python +def encrypt(message: str) -> str: + result: str = "" + message_in_caps: str = message.upper() + print(message_in_caps) + return result + +input_message: str = "Let us cycle tomorrow at 4pm starting from City Hall" +output: str = encrypt(input_message) +print(output) +``` + + + +We can observe that now we have `message_in_caps` with the message in all capital letters. Now, we are ready to start encrypting the letters. + +Now, we are going to iterate every character in `message_in_caps`. We are going to a few things for each character that will be repeated again and again. These are: + +``` +... +3. For each character in *message_in_caps* + 3.1 Get the character + 3.2 Change the character to ASCII number + 3.3 Subtract the ASCII number by 3 + 3.4 Change the number back to character, and + 3.5 Concatenate the character to *result* +``` + +Let's implement this iteration using the `for-in` statement. The variable on the left-hand side of the `in` operator captures the character for each iteration. + +```python +def encrypt(message: str) -> str: + result: str = "" + message_in_caps: str = message.upper() + for char in message_in_caps: + print(char) + +input_message: str = "Let us cycle tomorrow at 4pm starting from City Hall" +output: str = encrypt(input_message) +print(output) +``` + + + +In order to change the character to ASCII number as in step 3.2, we can use `ord()` function. Similarly, we can use the `chr()` function to change from ASCII to character to do step 3.4. + +```python +def encrypt(message: str) -> str: + result: str = "" + message_in_caps: str = message.upper() + for char in message_in_caps: + print(ord(char)) + +input_message: str = "Let us cycle tomorrow at 4pm starting from City Hall" +output: str = encrypt(input_message) +print(output) +``` + + + +So we can implement steps 3.2 to 3.5 in a single line as follows. + +```python +def encrypt(message: str): + result: str = "" + message_in_caps: str = message.upper() + for char in message_in_caps: + result += chr(ord(char) - 3) + print(result) + +input_message: str = "Let us cycle tomorrow at 4pm starting from City Hall" +output: str = encrypt(input_message) +print(output) +``` + + + +However, we noticed that the spacing is not preserved. Let's say, if we want to preserve the spacing in the encrypted message as a spacing as well, we can modify our algorithm as follows. + +``` +input: message -> string data type +output: encrypted message -> string data type +1. Create an empty string for output, name it as *result*. +2. Convert *message* to all capital letters, name it as *message_in_caps*. +3. For each character in *message_in_caps* + 3.1 Get the character + 3.2 If the character is a space + 3.2.1 Add a space to result + 3.3 Otherwise, + 3.3.1 Change the character to ASCII number + 3.3.2 Subtract the ASCII number by 3 + 3.3.3 Change the number back to character, and + 3.3.4 Concatenate the character to *result* +``` + +Now, we not only see iteration but we see a branch structure inside the iteration. + +```python +def encrypt(message: str): + result: str = "" + message_in_caps: str = message.upper() + for char in message_in_caps: + if char == " ": + result += char + else: + result += chr(ord(char) - 3) + print(result) + +input_message: str = "Let us cycle tomorrow at 4pm starting from City Hall" +output: str = encrypt(input_message) +print(output) +``` + + + +Now, we can see some spaces in the encrypted message. This may be easier to break, but that's ok. This encryption, is just for fun and it's good to illustrate how we can have a branch structure inside an iteration structure. + +All this while, we still use print to test our code. Now, we can return `result` as an output of the function when we are sure this is what we want for the `encrypt()` function. The final code looks something like the following. + +```python +def encrypt(message: str) -> str: + result: str = "" + message_in_caps: str = message.upper() + for char in message_in_caps: + if char == " ": + result += char + else: + result += chr(ord(char) - 3) + return result + +input_message: str = "Let us cycle tomorrow at 4pm starting from City Hall" +output: str = encrypt(input_message) +print(output) +``` + + + +Oops. There is something wrong with the code. Upon checking the output. It seems that `A` is translated to `>`. In our table, above, `A` should be translated as `X`. The reason is that we do not use modular arithmetic to calculate our translation. This means that our character can exceed the range that we have specified. To ensure that the character to be kept within `A-Z`, we can do the following modular arithmetic. + +```python +def encrypt(message: str) -> str: + result: str = "" + base: int = ord('A') + message_in_caps: str = message.upper() + for char in message_in_caps: + if char == " ": + result += char + else: + result += chr((((ord(char) - base) - 3) % 26 ) + base) + return result + +input_message: str = "Let us cycle tomorrow at 4pm starting from City Hall" +output: str = encrypt(input_message) +print(output) +``` + +What we did is simply set the ASCII number for `A` as the `base`. So we subtract any number by this base. This ensures the numbers are from 0 to 25 for 26 letters. Applying modulus 26 keeps the number within this range when it is subtracted by 3. When we are done, we add back `base` to get the right ASCII number. + + + +Now, we can see that `A` is translated correctly as `X`. diff --git a/_Notes/Graph_Search.md b/_Notes/Graph_Search.md deleted file mode 100644 index 30d68dd..0000000 --- a/_Notes/Graph_Search.md +++ /dev/null @@ -1,298 +0,0 @@ ---- -title: Graph Search -permalink: /notes/graph_search -key: notes-graph-search -layout: article -nav_key: Notes -sidebar: - nav: Notes -license: false -aside: - toc: true -show_edit_on_github: false -show_date: false ---- - -## Introduction - -In the previous lesson, we have introduced how we can represent a graph using object oriented programming. We created two classes `Vertex` and `Graph`. One main use cases with this kind of data is to do some search. For example, given the graph of MRT lines, we would like to search what is the path to take from one station to another station. We usually are interested in the shortest path. This is what Google Map and other Map application does. In this lesson, we will discuss two graph search algorithms: breadth-first search and depth-first search. - -To do these algorithms, our `Vertex` class and `Graph` class may need some additional attributes to store more information as it performs the search. One way to modify our classes is to create a new class. However, we would like to introduce the concept of **Inheritance**. Inheritance allows us to *re-use* our existing class and create a new class that is derived from our existing class. Therefore, to implement our search algorithm, we will use inheritance to modify our existing `Vertex` and `Graph` classes. - -## Breadth First Search - -Breadth first search is normally used to find a shortest path between two vertices. For example, when you plan your travel from one point to another point, breadth first search can identify the path you should take that gives the shortest path. How does this work. Let's take a look at the graph below. - -### (C)ases - - -drawing - -Let's say we want to find the shortest path from A to F. The way breadth-first search does is to calculate the distance of every vertex from A. So in this case, B will have a distance of 1 since it takes only one step from A to B. On the other hand, C has a distance of 2. D, however, has a distance of 1 since there is an edge from A to D connecting the two vertices directly. Vertex E has a distance 2 because from A it can go to D and then to E in two steps. Finally, F has a distance of 3. There are actually two paths from A to F. The first one is A - B - C - F and the second one is A - D - E - F. Notice that both has the same distance, which is 3. - -How can we obtain all the distances for each vertex? We will start with the starting vertex which in this case is A. Next, we can look into the neighbouring vertices that A has. So in this case, A has two neighbours, i.e. B and D. We can then explore each of the neighbours. We can draw the the vertex that we are exploring as a kind of a tree. - -drawing - -We can then take turn to explore the neighbours of each of the children in the tree. In this case, B has two neighbours, i.e. A and C. But since we have visited A, we do not want to visit A again. So we should only visit C. This indicates that we need to mark the vertices that we have visited. Similarly, D has three children, i.e. A, C and E. But both A and C has been visited, so we should just add E into our tree. We can continue the same steps until all vertices have been visited. The final tree looks like the following. - -drawing - -Notice that we can actually add F either to C or to E in the tree above. In this case, we choose to add to C. The question is when do we stop? We should stop when all the vertices have been explored in terms of their children. Therefore, we need some way to indicate if a Vertex's children have been fully explored. The way we are going to do this is to colour the vertices. Moreover, we are going to use three different colours: -- white: is used to indicate that we have not visited the vertex -- grey: is used to indicate that we have visited the vertex but we have not completely explored all the neighbours -- black: is when we have explored all the neighbours of this vertex. - -With this in mind, the image below shows the progression of the vertex exploration and how the colour of each vertex changes. - -drawing - -In the figures above, we use a Queue data structure to explore the vertices. When we visit vertex, we put all its neighbouring vertices into a queue. This also ensures that we explore the vertices in a **breadth-first** manner. For example, when we explore B from A, we did not go to C but rather D, which is at the same level as B in our search tree. This is where the name breadth-first search comes from. So now we are ready to write our algorithm for breadth-first search. - -### (D)esign of Algorithm - -``` -Input: -- G: Graph -- s: starting vertex -Output: -- Graph with distances on every vertex from s -Steps: -1. Initialize every vertex with the following: - 1.1 set color to white - 1.2 set distance to INF -2. start from s vertex: - 2.1 set s' color to grey - 2.2 set s' distance to 0 -3. put s into the Queue -4. As long as Queue is not empty - 4.1 take out one vertex from Queue and store to u - 4.2 for each neighbour of vertex u, do: - 4.2.1 if the neighbour colour is white, do: - 4.2.1.1 set the neighbour's colour to grey - 4.2.1.2 set the neighbour's distance to u's distance + 1 - 4.2.1.3 put the neighbour into the Queue - 4.3 after finish exploring all neighbours, set u's color to black -``` - -In the above algorithm, we start by setting all the vertices to white and have a distance of INF or any large number value greater than the number of the vertices. We then start from the vertex s and explore its neighbours. Everytime we explore a neighbour, we check its colour. If the colour is white, it means that it has not been visited previously, so we change the colour to grey and add the distance by one. We then put this neighbour into the queue to visit its neighbours later on. We proceed visiting the vertices by taking out the vertex from the Queue. As mentioned, it is the Queue that ensures that we visit the vertices in a breadth-first manner. - -The only thing about this algorithm is that we only get a graph with distances on each vertex, but we would not be able to retrieve the path to take from s to the destination vertex. In order to find the shortest path, we need to store the **parent** vertex when we visit a neighbouring vertex. To do this, we modify the algorithm as follows. - -``` -Input: -- G: Graph -- s: starting vertex -Output: -- Graph with: - - distances on every vertex from s - - parent vertex on every vertex that leads back to s -Steps: -1. Initialize every vertex with the following: - 1.1 set color to white - 1.2 set distance to INF - 1.3 set parent to NILL -2. start from s vertex: - 2.1 set s' color to grey - 2.2 set s' distance to 0 - 2.3 set s' parent to NILL -3. put s into the Queue -4. As long as Queue is not empty - 4.1 take out one vertex from Queue and store to u - 4.2 for each neighbour of vertex u, do: - 4.2.1 if the neighbour colour is white, do: - 4.2.1.1 set the neighbour's colour to grey - 4.2.1.2 set the neighbour's distance to u's distance + 1 - 4.2.1.3 set u as the parent vertex of the neighbour - 4.2.1.4 put the neighbour into the Queue - 4.3 after finish exploring all neighbours, set u's color to black -``` - -In the second algorithm, we have a new attribute called **parent**. In the beginning we set all vertices to have NILL as their parents. Since s is the starting vertex, it has no parent and so we set to NILL in step 2.3. We added step 4.2.1.3 where we set u as the parent to the neighbouring vertex when we add that neighbouring vertex into the Queue. - -With this, we can write another algorithm to retrieve the path from s to some destination vertex v. - -``` -Find-Path BFS -Input: -- G: graph after running BFS -- s: start vertex -- v: end vertex -Output: -- list of vertices that gives the shortest path from s to v -Steps: -1. if v is the same as the start vertex - 1.1 return a list with one element, i.e. s -2. otherwise, check if parent of v is NILL - 2.1 return "No path from s to v exist" -3. otherwise, - 3.1 call find-path(G, s, parent of v) - 3.2 add v into the result from 3.1 -``` - -The above algorithm uses recursion. There are two base cases. The first base case is when the destination vertex to be the same as the starting vertex. In this case, the output is just that vertex. The second base case is when there is no path from s to v. We know there is no path when along the path starting from v we find a vertex which parent is NILL. The recursion case is described in step 3. In this case, we call the same function but with the destination vertex to be the parent of the current destination vertex. By moving the destination vertex to the parent, we reduce the problem and make it smaller until we reach base case described in step 1. - -Let's see an example when we search a shortest path from A to F in the previous graph. In this case, v is not the same as s since A and F are two different vertices. So we look into F's parent, which is C. Now we call the same function to find the path from A to C. Since A and C are different, we look into C's parent, which is B and call the function to find the path from A to B. Finally, we look into B's parent and find the path from A to A. This is the base case. When we reach the base case, we return a list containing A as the result (step 1.1). Then we move back and do step 3.2 to add B, C, and finally F. So the shortest path from A to F will be a list `[A, B, C, F]`. - -Now it's time to implement the algorithm. But before we can implement the algorithm, we need to modify the class `Vertex` to contain a few additional attributes: -- colour -- distance -- parent - -As mentioned in the Introduction, we will do this using the concept of **Inheritance**. - -## Inheritance - -Inheritance is an important concept in object oriented programming that allows us to re-use existing code or classes we have written. In our example here, we already have a class `Vertex` and `Graph`. However, the class `Vertex` only contains `id` and `neighbours`. - -![](/assets/images/week5/Graph_Vertex_Specification.jpg) - -As shown in the previous section, our `Vertex` object has additional attributes such as *colour*, *distance*, and *parent*. We can actually create a new class containing all these new properties as well as the commonly found properties of a vertex (`id` and `neighbours`). However, we will duplicate our codes and rewriting the methods that is the same for all `Vertex` objects such as adding a neighbour. Inheritance allows us to create a new class without duplicating all the other parts that is the same. By using inheritance, we create a new class by *deriving* it from an existing base class. In this example, we can create a new class called `VertexSearch` that is derived from a base class `Vertex`. When a class inherits another class, the new class possess all the attributes and methods of its parent class. This means that `VertexSearch` class contains both `id` and `neighbours` as well as all the methods that `Vertex` class has. What we need to do is simply to specify what is different with `VertexSearch` that `Vertex` class does not have. We can represent this relationship using a UML diagram as shown below. - -drawing - -In the UML diagram, the relationship is represented as an arrow with a white triangle pointing from the child class to the parent class. This relationship is also called **is-a** relationship which simply means that `VertexSearch` *is-a* `Vertex`, it has all the attributes and methods of a `Vertex` object. - -In Python, we can specify if a class derives from another class using the following syntax: - -```python -class NameSubClass(NameBaseClass): - pass -``` - -For our example here, we can write the `VertexSearch` class as follows. - -```python -class VertexSearch(Vertex): - pass -``` - -In the above class definition, we have `Vertex` inside the parenthesis to indicate to Python that we will use this as the *parent* class or *base* of `VertexSearch` class. In this new class, we can initialize all the new attributes as usual: - -```python -import sys - -class VertexSearch(Vertex): - def __init__(self, id=""): - super().__init__(id) - self.colour = "white" - self.d = sys.maxsize - self.parent = None -``` - -Notice here that we initialize: -- `colour` to be "white" -- `d` to be a large integer number -- `parent` to be a `None` object - -The first line `super().__init__(id)` is to call the parent class' initialization method to initialize the both the `id` and the `neighbours`. The word `super` comes from Latin which means "above". Therefore, `super()` method returns a reference to the parent's class. Since we have both `__init__()` method in `VertexSearch` and `Vertex`, we need to be able to differentiate the two methods. For this purpose, Python provides `super()` method to refer to the parent's class methods instead of the current class. - -In the child class `VertexSearch` we redefine the `__init__()` method of the parent's class. This is what is called as **method overriding**. We will discuss more of Inheritance in future lessons. - -## Depth-First Search - -There is another kind of search that can be done on a graph. This is called **depth-first** search. As the name implies, this algorithm explores the neighbouring vertices in a depth-wise manner. Let's illustrate this with the same graph as we have seen previously. - -### (C)ases - -drawing - -In depth-first search, we go down the tree before moving to the next siblings. For example, as we start from A, we look into its neighbouring vertices. So vertex A has two neighbours, i.e. B and D. The depth-first search algorithm will visit one of them, say vertex B. After it visits B, it will explore one of the neighbours of B instead of visiting D. This is illustrated below. - -drawing - -In the figures above, every time we visit a vertex, we put a timestamp on that vertex called **discovery time**. Once we finish visiting all the neighbours of that vertex, we put another timestamp called **finishing time**. For example, vertex A has a discovery time 1 and finishing time 12 as indicated by 1/12 in the figure. - -We also labelled the edges with two different kind of symbols. The solid line edges are called **tree edges**. These are edges in the depth-first forest. An edge (u, v) is a tree edge if v was first discovered by exploring edge (u, v). For example, the edge (A, B) is a tree edge since B is first discovered by exploring the edge (A, B). On the other hand the edge (A, D) is not a tree edge since D was not first discovered by exploring edge (A, D). Rather, D was first discovered by exploring the edge (C, D). - - -This brings us to the other kind of edges discovered by depth-first search. The dashed line edges are called **back edges**. These are those edges connected a vertex u to an ancestor v in a depth-first tree. For example, A is an ancestor of D. We can see that because we explore D through A - B - C - D. So the edge connecting D to A is a back edge since it connects D to one of its ancestor. Similarly with the edge (C, F). The depth-first forest is shown below. - -drawing - -### (D)esign of Algorithm - -Now we can try to write the steps to do depth-first search. We will write the steps using two functions. The first one is called DFS as shown below. - -``` -DFS -Input: -- G: graph -Output: -- G: graph with the following attributes marked - - colour - - discovery time - - finishing time - - parent -Steps: -1. Initialize each vertex as follows: - 1.1 set colour to white - 1.2 set parent to NILL -2. set time to 0 -3. for each vertex in the graph G - 3.1 if the vertex's colour is white, do: - 3.1.1 dfs-visit(G, u) -``` - -The above algorithm simply initialize the vertices and go through every vertex to perform the second function DFS-VISIT. - -``` -DFS-Visit -Input: -- G: graph -- u: vertex to visit -Output: -- G: graph with the following attributes marked - - colour - - discovery time - - finishing time - - parent -Steps: -1. increase time by 1 -2. set current time to be the discovery time for u -3. set u's colour to gray -4. for each vertex in u's neighbours, do: - 4.1 if the vertex's colour is white, do: - 4.1.1 set u as the parent of the vertex - 4.1.2 call dfs-visit(G, the current vertex) -5. set u's colour to black -6. increase time by 1 -7. set current time to be the finishing time -``` - -This function simply set the discovery time for the visited vertex u and begins to visit all the neighbouring vertices of u. However, it only calls DFS-VISIT if the neighbouring vertices are white, which means these vertices have not been visited yet. Once it finishes visiting all the neighbouring vertices, it marks the vertex u to be black and set the finishing time. - -## Topological Sort - -One application of depth-first search algorithm is to perform a topological sort. For example, if we have list of task with dependencies, we can sort which task should be performed first. The figure below gives an example of this dependencies tasks - -![](/assets/images/week5/topological_sort_graph.jpg) - -The figure above shows a directed graph of dependencies between different tasks. For example, the task wearing a pant must be done only after the task of wearing underpant and wearing a shirt. We can use the finishing time of DFS to determine the sequence of tasks. - -Let's try to perform DFS for the above graph. The discovery time and the finishing time for each task is shown in the figure below. - -![](/assets/images/week5/topological_sort_finishingtime.jpg) - -In the process of DFS, it somehow starts with "undershirt" and traverse to all the children vertices in the tree, i.e. "pants", "wallets", "belt", and then "shoes". After this, it creates another tree starting from "socks", and then another tree starting from "watch", and finally another tree starting from "underpant". The depth-first forest looks like the figure below. - -![](/assets/images/week5/depth_first_forest.jpg) - -We can re-order the tasks by its finishing time from the largest to the smallest as shown in the figure below. - -![](/assets/images/week5/sorted_graph.jpg) - -The sequence above is based on its finishing time from the largest to the smallest. The first three are independent and their sequence can be interchanged, but subsequently, "shirt" must be done only after "undershirt" task. This sequence may also depends on which vertex the search encounters first. With this in mind, we can write the topological sort steps as follows. - -``` -Topological-Sort -Input: -- G: graph -Output: -- list of sorted vertices -Steps: -1. call DFS(G) to compute the finishing time for each vertex in G -2. sort the vertices based on its finishing time from largest to smallest -3. return a list of sorted vertices -``` - diff --git a/_Notes/Hello_World.md b/_Notes/Hello_World.md new file mode 100644 index 0000000..4b2aff6 --- /dev/null +++ b/_Notes/Hello_World.md @@ -0,0 +1,128 @@ +--- +title: Hello World +permalink: /notes/hello-world +key: notes-hello-world +layout: article +nav_key: Notes +sidebar: + nav: Notes +license: false +aside: + toc: true +show_edit_on_github: false +show_date: false +--- + +## Your First Problem + +Instead of starting with the first computer code, we would like to start with the first *problem*. Computing centres around data and the first problem is the following: *How to display data into your computer screen?* + +Whenever you work with computer, you notice that there are a few components: +- input devices +- output devices +- compute devices + +A computer comprises of all these three and you will see that any problem statement tend to comprise the following: +- what is the input? +- what is the output +- what is the computation or the process? + +So we have actually introduced the first step in PCDIT problem solving framework. In this framework, the first step is to specify the problem statement. In specifying the problem statement, we need to answer the three questions above: the input, the output and the process. + +So we can rewrite our first problem and make it more specific. What kind of data are we referring to? We have identified the process is to display this data into the output device, which is your computer screen. But what to be displayed? In many programming lessons, the first thing that programmers usually display is the following data: "Hello World!". + +Maybe, we should stop and ask what kind of data is "Hello World!". But before we go deeper, let's give a teaser to the solution of this problem written in Python. We will dig in deeper into this simple solution in the following sections. For now, here is how to display "Hello World!" into your computer screen. + +```python +print("Hello World!") +``` + +That's it! But how to run this code? + +## Running a Python Code + +We have written our first computer code in Python. But recall that what we have created is only the *source code*. The source code does not do anything. The code has to be executed for the computer to do what you ask the computer to do. Recall that there are two kinds of programming language: interpreter and compiler. Python is an interpreted language. This means that we need a Python interpreter to execute the code and make the computer to do what we ask it to do. + +There are two main different ways of executing a Python code and they are listed here: +- Python Shell +- Python Script + +If you have installed Python in your computer, you can go to your computer terminal or command prompt and type the following: +```sh +python +``` + +Some operating system who keeps Python 2 and Python 3 version may require you to specify the version. For example, in Ubuntu Linux OS, you run the Python shell for Python version 3 using the following. + +```sh +python3 +``` + +Once you run the Python interpreter, you will see the Python shell where you can type in your Python code into the prompt. It looks like the following: + +```sh +Python 3.11 (default, April 4 2021, 09:25:04) +[GCC 10.2.0] on linux +Type "help", "copyright", "credits" or "license" for more information. +>>> +``` + +The version and the other details maybe different from your Python installation. However, at the end of the shell, you will see the Python shell's prompt `>>>`. + +If you type in the code above into this shell (don't forget to press the ENTER key at the end), you will see that the computer is producing the output into the screen immediately below it. + +```sh +>>> print("Hello World!") +Hello World! +>>> +``` + +We'll show you how to run Python's Script but for the next few section you can just type your code into the Python's shell. + +## Human Language and Programming Language + +At this point it is important to note that there is a significant different between our human language and a programming language. The reason for this difference is that computer is not as smart as human in resolving ambiguity in the message. Computer requires a more exact instruction that is non-ambiguous to execute your instruction. On the other hand, human is much better at resolving ambiguities by reading the context, body language and other clues. Human also can verify if his understanding is correct or not. Computer requires exact and non-ambiguous instruction. + +This means that your *source code* has to be written with a certain rules. We call this *syntax*. For example, if you missed out one single character, Python interpreter may complain. Try to remove the last paranthesis and type it again into the shell. You will see the following behaviour. + +```sh +>>> print("Hello World!" +... +... +... +``` + +In the above situation, Python interpreter expects you to have a closing parenthesis and when there is no closing parenthesis, the Python interpreter will wait for you to close it. To end the above misery just type in the closing parenthesis `)` and press enter in the shell. + +Why do you need a closing parenthesis? We will talk about **function** in more detail, but for now, it is sufficient to say that `print()` is one of Python built-in *functions*. To *invoke* or *call* a function, you need to specify the function name (in this case is `print`) and use parenthesis (opening and closing round bracket `()`) after the function name. So `print()` is the *syntax* to invoke the `print` function. You can guess that `print()` function's job is to display or print data into the standard output of your computer, which in most cases is the screen. What data to be displayed is the *argument* of the function and is put in between the parenthesis. This is why Python expect an opening and closing parenthesis to know when is the end of the function call. + +You can also try to remove the last double quotes after the exclamation mark. This is the output. + +```sh +>>> print("Hello World!) + File "", line 1 + print("Hello World!) + ^ +SyntaxError: EOL while scanning string literal +>>> +``` + +The above shows our first error message. Sometimes it's easier to read from the bottom to the top when dealing with error message. The last line mentioned that the error is the following. + +``` +SyntaxError: EOL while scanning string literal +``` + +This indicates that the type of error is a `SyntaxError`. The message after this is saying that Python interpreter finds `EOL` which stands for *end of line* while scannign the string literal `"Hello World!`. What happens is that Python interpreter sees the first double quotes before the letter `H` and recognize that you are trying to create a string literal. However, Python interpreter cannot find the ending of this string literal before it encounters the end of line. To fix this error, you need to put back the closing double quotes after the exclamation mark. + +## From a Hello World to a ChatBot + +You may get bored with saying hello world to the multiverse. But if you have played with any chatbot, that maybe the first thing a chat bot does. A chat bot can show a message to greet you. + +Let's create our first chat bot greeting then. + +```python +print("Hello Jane! Welcome to the multiverse.") +``` + +If your name is not Jane, you maybe dissapointed. In the next lesson, we will learn how to take in your name so that the chatbot can greet you using your name. But before that, we need to ask what kind of data is it? What kind of data a name is? As we build our chatbot, we will involve more and more data. \ No newline at end of file diff --git a/_Notes/Inheritance_ABC.md b/_Notes/Inheritance_ABC.md deleted file mode 100644 index ca02546..0000000 --- a/_Notes/Inheritance_ABC.md +++ /dev/null @@ -1,177 +0,0 @@ ---- -title: Inheritance and Abstract Base Class -permalink: /notes/inheritance_abc -key: notes-inheritance-abc -layout: article -nav_key: Notes -sidebar: - nav: Notes -license: false -aside: - toc: true -show_edit_on_github: false -show_date: false ---- - -## Inheritance - -In the previous lesson, we have shown that we can reuse the code from some *base* class by using inheritance. The syntax in Python for deriving a class from some base class is as follows: - -```python -class NameSubClass(NameBaseClass): - pass -``` - -The name of the parent class or the base class is specified in the parenthesis after the class name. By specifying this, the child class inherits all the *attributes* and *methods* of the parent class. So what do we define in the child class? We can define the following things: -- attributes and methods that are unique to the child class -- methods in the parent's class that we want to override - -One example that we had in the previous lesson is to create the class `VertexSearch` from the class `Vertex`. The class `Vertex` has two attributes: `id` and `neighbours`. When the class `VertexSearch` inherits from `Vertex`, any object of `VertexSearch` also has `id` and `neighbours`. What we need to define in the class `VertexSearch` are those attributes not present in the parent class. In this example, `VertexSearch` has additional attributes of `colour`, `distance`, and `parent`. Now, `Vertex` in general will not have these attributes since these are only used when doing a graph search. Similarly, we can also define any additional *methods* in the child class that is present in the parent class. - -Besides defining attributes and methods that are unique to the child class, we can also *re-define* the methods of the parent class. This is what is called as *overriding*. One common method that is usually overridden is the initialization method. - -```python -class Vertex: - def __init__(self, id=''): - self.id = id - self.neighbours = {} -``` - -And the class `VertexSearch` can override this initialization: -```python -import sys - -class VertexSearch(Vertex): - def __init__(self, id=""): - super().__init__(id) - self.colour = "white" - self.d = sys.maxsize - self.f = sys.maxsize - self.parent = None -``` - -The first line of the init is to call the *parent* class' initialization and the subsequent lines proceed to initialize those attributes that is unique to the child class. In this way, we need not re-write all the initialization codes of the parent class and simply re-use them. Note that in overriding a method in the parent class, we use the same method's name and arguments as in the parent class. - -Let's discuss a few more examples of inheritance. - -## Fraction and MixedFraction - -Let's say we have a class called `Fraction` which has two attributes: *numerator* and *denominator*. This class also has all the methods to do the operation such as addition and subtraction. With this we can do addition and subtraction of Fraction: - -```python -f1 = Fraction(1, 2) -f2 = Fraction(3, 4) -f3 = f1 + f2 -f4 = f2 - f1 -``` - -The first line creates a fraction object $1/2$ while the second line creates a fraction object $3/4$. The third line adds these two fractions $1/2 + 3/4 = 5/4$ which is then stored in `f3`. The last line, on the other hand, subtracts these two fractions, $3/4 - 1/2 = 1/4$ which is then stored in f4. - -What should we do if we want to do operation with a mixed fraction such as the following? - -$$1 \frac{1}{2} + 2\frac{3}{4}$$ - -Well, we can always represent these mixed fraction as two ordinary fractions - -$$\frac{3}{2} + \frac{11}{4}$$ - -and perform the same fraction operations. However, we do not want to do this manipulation or conversion manually if we can just write a computer code to do so. Therefore, it is worthwhile to create a new class called `MixedFraction` where we can define a fraction that may contain a whole number and additional numerator and denominator. What is different from the `Fraction` class is the way we initialize this object. Using the example above, i.e. $1 \frac{1}{2} + 2\frac{3}{4}$, we want to be able to write in the following manner: - -```python -f1 = MixedFraction(1, 2, 1) -f2 = MixedFraction(3, 4, 2) -f3 = f1 + f2 -``` -Note that we purposely put the whole number as the last argument because we want `MixedFraction` to be able to handle ordinary fraction when the whole number is zero. - -```python -f4 = MixedFraction(1, 2) # this is the same as Fraction(1, 2) -``` -The UML class diagram can be seen as shown below. - -drawing - -In the above UML diagram, we choose not to have any additional attributes but only different initialization arguments. This means that we have to initialize the numerator and the denominator from the three arguments used in the initialization `MixedFraction(top, bottom, whole)`, i.e. - -$$numerator = whole \times bottom + top$$ - -Similarly, there are no methods to do addition and subtraction. The object of `MixedFraction` depends on its parent class' methods to do addition and subtraction. In fact, when Python cannot find the name of a particular method in the child class, it will try to find the same name in the parent class' methods. If no name is matched in the parent class' methods, Python will throw an error saying that such method is not defined. - -Moreover, we also choose to implement `__str__()` method which is called whenever Python tries to convert the object to an `str` representation. Notice that we choose to **override** this method in the child class. The reason is that we want `Fraction` and `MixedFraction` to be represented differently as a string. For example, $5/2$ will be represented differently depending whether it is a `Fraction` object or a `MixedFraction` object. - -```python -5/2 # str representation of Fraction -2 1/2 # str representation of MixedFraction -``` - -This is an example of how a parent class' method is overriden in the child class. The name and the argument of the method is the same and yet the behaviour is different in the child class. - -Now, let's look at another example - -## Queue and Deque - -Another example we can work on is to extend the class `Queue` to implement a new data structure called `Deque` (pronounced as deck). The difference between a `Queue` and a `Deque` is that in `Queue` the item only has one entrance which is from the back of the Queue. The exit of a `Queue` object is at the front of the Queue. On the other hand, a `Deque` can be inserted other from the front or from the rear. Its item also can be popped out from either the front or the rear. Below is the UML representation of the class diagram when `Queue` is implemented using a double Stack. - -drawing - -Notice that in the above UML class diagram, we use `/` to represent computed property, i.e. `/size` and `/is_empty`. `Deque` does not have any additional attributes or property. The only changes are the methods. We rename and add additional methods for `Deque` class. In this cass, `add_rear(item)` of `Deque` is the same as `enqueue(item)` of a `Queue` object. Similarly, `remove_front()` method of `Deque` is the same as `dequeue()` of a `Queue` object. This is also true for the case of `peek_front()` and `peek()`. Thus, we need not re-write half of the methods in `Deque` class since we can simply call its parent class' methods. - -## Abstract Base Class - -There are cases when the *parent* class only specifies what attributes and methods the child classes should have and in itself contain no implementation. You can think of this as something like the following definition: - -```python -class MyAbstractClass: - def add(self, other): - pass - -class ChildOfMyAbstractClass(MyAbstractClass): - def add(self, other): - # contains implementation of adding the two objects - ... -``` - -In the first class of `MyAbstractClass`, there is a method `add(other)` which contains no definition. This method is overriden by the child class `ChildOfMyAbstractClass`. In this class, `add(other)` is defined and, thus, overridden. - -Previously in `MixedFraction` class, we see how the child class' operations depends on the implementation of its parent class' methods. In that case, no `__add__()` nor `__sub__()` is defined in the child class. Therefore any method call to do addition and subtraction will be referred to the parent class implementation. The case of an Abstract class is the opposite of this. When we have an Abstract class with no implementation, we are forcing the implementation to be found in the child class. However, by writing the code as shown above, there is nothing that prevents the child class **not** to implement the required method. - -Python provides some mechanism to ensure that the abstract method in the abstract base class is implemented in the child class. Let's take a look at one example of this using `collections.abc` class. This `collections.abc` class is an Abstract Base Class for containers. For example, if we want to create a new data type belonging to a type `Iterable`, we can inherit this new class from `collections.abc.Iterable`. Python will force the new class to define the method `__iter__()`. Otherwise, Python will throw an error. Let's try it out in the next cell. - -```python -import collections.abc as c - -class NotRightIterable(c.Iterable): - def __init__(self): - self.data = [] - -test = NotRightIterable() -``` - -The output is -```sh ---------------------------------------------------------------------------- -TypeError Traceback (most recent call last) - in - 5 self.data = [] - 6 -----> 7 test = NotRightIterable() - -TypeError: Can't instantiate abstract class NotRightIterable with abstract methods __iter__ -``` -When you run the above cell, Python will complain saying that it cannot instantiate the new class because we did not implement the abstract method `__iter__()`. To fix this, we need to define this method in the child class. - -```python -import collections.abc as c - -class RightIterable(c.Iterable): - def __init__(self): - self.data = [] - - def __iter__(self): - return iter(self.data) - -test = RightIterable() -``` -There will be no error when you run the above cell because now the method `__iter__()` has been implemented in the child class. The definition of `__iter__()` simply returns an iterable object from `self.data`. - -So, we have shown the mechanism where Python ensures that when you create an Abstract Base Class with some abstract methods, the child class must implement this abstract method. Otherwise, Python will throw an exception. In future lessons, we will create our own Abstract Base Class. \ No newline at end of file diff --git a/_Notes/Intro_to_Graph.md b/_Notes/Intro_to_Graph.md deleted file mode 100644 index 05a6770..0000000 --- a/_Notes/Intro_to_Graph.md +++ /dev/null @@ -1,102 +0,0 @@ ---- -title: Introduction to Graph -permalink: /notes/intro_graph -key: notes-intro-graph -layout: article -nav_key: Notes -sidebar: - nav: Notes -license: false -aside: - toc: true -show_edit_on_github: false -show_date: false ---- - -## What is a Graph? - -In previous sections, we have worked with various algorithms and data. For example, we did sorting algorithm in a sequence of data of a list or array-like type. List and array is one kind of data where the item has relationship only with its previous and next item in a sequence. Stack and Queues are another kind of data structures. Even with these two, each item is related only in linear fashion, either with the next one at the top of the Stack or with the next in the sequence of the Queue. A Graph allows more relationship to be represented between each item. Two examples of graph data structures are shown below. - -Train Station Graph -Control Flow Graph - - -In the first example, the graph represent a kind of connection between places like in a map. With this kind of data, we can find a path from one place to another place or finding the shortest distance between two places. In the second example, the graph represent the control flow of a computer program. Compiler can use this information to optimize the code. Both are a Graph data type that represent different things. We can define a few things when dealing with a Graph. - -Image of an Abstract Graph - -- **Vertex**: A vertex is a node that is connected by edges in a graph. A vertex can have a name which is also called its "key". In the above example, V1, V2, V3, etc are the vertices. -- **Edge**: An edge in the figure above is represented by the lines connecting two vertices. An edge can be uni-directinal or bi-directional. The direction is usually represented by the arrow. Bi-directional edges usually do not have arrow heads. In the above examples, E1, E2, E3, etc are edges. Note that E1 and E6 are bi-directional while the rest are uni-directional. - - -## How to Represent a Graph in a Code? - -In the previous section we show some examples how real-world data like train stations or even a computer code can be represented as a graph. In this section we would like to discuss how such graphs are written in a computer code. The main information needed by the computer is the following: -- what are the vertices -- what are the edges -- how the vertices are connected by the edges - -### Adjacency Matrix - -One way to represent this is to use an **Adjencency Matrix**. In this matrix, if there is a connection between one vertex to another, the cell between that row and column is represented by some number, e.g. 1 instead of 0 as when there is no connection. For example, the last graph above can be written in the following matrix: - -| | V1 | V2 | V3 | V4 | V5 | -|----|----|----|----|----|----| -| V1 | | 1 | | | 1 | -| V2 | 1 | | 1 | 1 | | -| V3 | | 1 | | | | -| V4 | 1 | | 1 | | | -| V5 | | | | 1 | | - -Note the following: -- The connection from vertex *u* to vertex *v* is represented by a non-zero value at row *u* and column *v*. -- For example, there is an edge from V1 to V2, so there is a 1 entry at row V1 and column V2. Similarly, there is an edge from V4 to V3 and this is represented by a 1 at row V4 and column V3. -- If the edge is bi-directional, we have a symmetry in the entry. For example, V1 is connected to V2 with a bi-directional edge. We see a non-zero entry at row V1 and column V2 as well as row V2 and column V1. Similarly between V2 and V3. - -The advantage of this representation is that it is simple and intuitive. The only thing is that it may end up in a sparse matrix where most of the entry are zeros and only a few non-zero entry. So this is good when the number of edges is large such as when every vertex is connected to every other vertices. - -### Adjacency List - -Another way to represent a graph is to use **adjacency list**. This is more suitable when the number of edges is not large. We can use a dictionary for this purpose: - -```python -graph1 = {'V1': ['V2', 'V5'], - 'V2': ['V1', 'V3', 'V4'], - 'V3': ['V2'], - 'V4': ['V1', 'V3'], - 'V5': ['V4']} -``` - -Notice that the keys are all the vertices in the graph and the value of the dictionary is a list of all the adjacent vertices connected to that particular vertex. For example, vertex V1 is connected to two other vertices V2 and V5. In fact, since there is no particular sequence for the adjacent vertices, you need not use a list and can use a dictionary instead as in the following: - -```python -graph1 = {'V1': {'V2': 1, 'V5': 1}, - 'V2': {'V1': 1, 'V3': 1, 'V4': 1}, - 'V3': {'V2': 1 }, - 'V4': {'V1': 1, 'V3': 1}, - 'V5': {'V4': 1}} -``` - -The value in the dictionary of the adjacent vertices are all 1 for this example, but they need not be. These values are called the **weights** or the **costs**. You can assign different weights. For example, in the graph for the MRT train, you can assign more cost to connection between Tampines Downtown Line to Pasir Ris or Simei East West Line if passanger has to go out from the MRT station from one line to the other line. - -### Using Object Oriented Programming - -You have learnt Object Oriented programming in the previous week. We can apply this programming concept to represent a graph. We can create two classes: -- `Vertex` class -- `Graph` class - -The `Vertex` class is similar to each entry in the dictionary. This class contains information on that particular vertex and who are the neighbouring or adjacent vertices connected this particular vertex. This class can also contains the weights of the connection between this vertex to its neighbours. The `Graph` class, on the other hand, contains the list of all the vertices in the graph. Each of this vertex is of the type `Vertex`. We can draw the UML diagram of these two classes as follows. - -drawing - -The above UML diagram shows that a `Graph` is composed of one or more `Vertex` objects. This is another *composition* relationship between two classes. - -We can specify the attributes and methods for both classes as shown in the image below. - -drawing - -The class `Graph` has an attribute called `vertices`. This attribute contains all the vertices in the graph where each vertex is of the type `Vertex`. This class has several methods like how to create or retrieve a `Vertex` object in the graph, add an edge between two vertices given their starting and ending `id`s. It may also have some other helper methods like to get all the neighbouring vertices of a given `Vertex` or to get the number of vertices in the graph. You can design some other methods but these are some of the common operations we may want to perform with a graph. - -The class `Vertex` has two attributes. The first one is the `id` or the label for the `Vertex` object and the second one is its neighbouring `Vertex` objects. The class has some basic operation such as to add a neighbouring `Vertex` to the current `Vertex` object, or to get all the neighbouring `Vertex` objects of the current Vertex. Lastly, it also has a method to get the weight of the edge to the neighbouring `Vertex` object. Similarly, you can think of some other operations of a `Vertex` object that may not be listed above. - - diff --git a/_Notes/LinearRegression.md b/_Notes/LinearRegression.md deleted file mode 100644 index efd4e89..0000000 --- a/_Notes/LinearRegression.md +++ /dev/null @@ -1,321 +0,0 @@ ---- -title: Linear Regression -permalink: /notes/linear_regression -key: notes-linear-regression -layout: article -nav_key: Notes -sidebar: - nav: Notes -license: false -aside: - toc: true -show_edit_on_github: false -show_date: false ---- - -## Introduction - -Linear regression is a machine learning algorithm dealing with a continuous data and is considered a supervised machine learning algorithm. Linear regression is a useful tool for predicting a quantitative response. Though it may look just like another statistical methods, linear reguression is a good jumping point for newer approaches in machine learning. - - -In linear regression we are interested if we can model the relationship between two variables. For example, in a HDB Resale Price dataset, we may be interested to ask if we can predict the resale price if we know the floor size. In the simplest case, we have an independent variable $x$ and a dependent variable $y$ in our data collection. The linear regression algorithm will try to model the relationship between these two variables as a straight line equation. - -$$ y = m x + c$$ - -In this sense, the model consists of the two coefficients $m$ and $c$. Once we know these two coefficients, we will be able to predict the value of $y$ for any $x$. - -## Hypothesis - -We can make our straight line equation as our hypothesis. This simply means we make a hypothesis that the relationship between the independent variable and the dependent variable is a straight line. To generalize it, we will write down our hypothesis as follows. - -$$y = \beta_0 + \beta_1 x$$ - -where we can see that $\beta_0$ is the constant $c$ and $\beta_1$ is the gradient $m$. The purpose of our learning algorithm is to find an estimate for $\beta_0$ and $\beta_1$ given the values of $x$ and $y$. Let's see what this means on our actual data. Recall that we have previously work with HDB Resale price dataset. We will continue to use this as our example. In the codes below, we read the dataset, and choose resale price from TAMPINES and plot the relationship between the resale price and the floor area. - - - -```python -import pandas as pd -import matplotlib.pyplot as plt -import seaborn as sns -import numpy as np -``` - - -```python -file_url = 'https://www.dropbox.com/s/jz8ck0obu9u1rng/resale-flat-prices-based-on-registration-date-from-jan-2017-onwards.csv?raw=1' -df = pd.read_csv(file_url) -df_tampines = df.loc[df['town'] == 'TAMPINES',:] -sns.scatterplot(y='resale_price', x='floor_area_sqm', data=df_tampines) -``` - - - - - - - - - -![png](/assets/images/week9/LinearRegression_4_1.jpeg) - - -Notice that the resale price increases as the floor area increases. So we can make a hypothesis by creating a straight line equation that predicts the resale price given the floor area data. The figure below shows the plot of a straight line and the existing data together. - - -```python -y = 52643 + 4030 * df['floor_area_sqm'] -sns.scatterplot(y='resale_price', x='floor_area_sqm', data=df_tampines) -sns.lineplot(y=y, x='floor_area_sqm', data=df_tampines, color='orange') -``` - - - - - - - - - -![png](/assets/images/week9/LinearRegression_6_1.jpeg) - - -Note that in the above code, we created a straight line equation with the following coefficients: -$$\beta_0 = 52643$$ -and -$$\beta_1 = 4030$$ - - -In machine learning, we call $\beta_0$ and $\beta_1$ as the model *coefficents* or *parameters*. What we want is to use our training data set to produce estimates $\hat{\beta}_0$ and $\hat{\beta}_1$ for the model coefficients. In this way, we can **predict** future resale prices by computing - -$$\hat{y} = \hat{\beta}_0 + \hat{\beta}_1 x$$ - -where $\hat{y}$ indicates a prediction of $Y$. Note that we use the *hat* symbol to denote the estimated value for an unknown parameter or coefficient or to denote the predicted value. The predicted value is also called a hypothesis. - -## Cost Function - -In order to find the values of $\hat{\beta}_0$ and $\hat{\beta}_1$, we will apply optimization algorithm that minimizes the error. The error caused by the difference between our predicted value $\hat{y}$ and the actual data $y$ is captured in a *cost function*. Let's find our cost function for this linear regression model. - -We can get the error by taking the difference between the actual value and our hypothesis and square them. The square is to avoid cancellation due to positive and negative differences. This is to get our absolute errors. For one particular data point $i$, we can get the error square as follows. - -$$e^i = \left(\hat{y}(x^i) - y^i\right)^2$$ - -Assume we have $m$ data points, we can then sum over all data points to get the Residual Sum Square (RSS) of the errors. - -$$RSS = \Sigma_{i=1}^m\left(\hat{y}(x^i) - y^i\right)^2$$ - -We can then choose the following equation as our cost function. - -$$J(\hat{\beta}_0, \hat{\beta}_1) = \frac{1}{2m}\Sigma_{i=1}^m\left(\hat{y}(x^i) - y^i\right)^2$$ - -The division by $m$ is to get an average over all data points. The constant 2 in the denominator is make the derivative easier to calculate. - -The learning algorithm will then try to obtain the constant $\hat{\beta}_0$ and $\hat{\beta}_1$ that minimizes the cost function. - -$$\begin{matrix}\text{minimize} & J(\hat{\beta}_0, \hat{\beta}_1)\\ -\hat{\beta}_0, \hat{\beta}_1\\ \end{matrix}$$ - -## Gradient Descent - -One of the algorithm that we can use to find the constants by minimizing the cost function is called *gradient descent*. The algorithm starts by some initial guess of the constants and use the gradient of the cost function to make a prediction where to go next to reach the bottom or the minimum of the function. In this way, some initial value of $\hat{\beta}_0$ and $\hat{\beta}_1$, we can calculate the its next values using the following equation. - -$$\hat{\beta}_j = \hat{\beta}_j - \alpha \frac{\partial}{\partial \hat{\beta}_j} J(\hat{\beta}_0, \hat{\beta}_1)$$ - -In order to understand the above equation, let's take a look at a two countour plot below. - - - - -The contour plot shows the minimum somewhere in the centre. The idea of gradient descent is that we move the fastest to the minimum if we choose to move in the direction with the steepest slope. The steepest slope can be found from the gradient of the function. Let's look at point $x_0$ in the figure. The gradient in the direction of $\beta_0$ is non zero as can be seen from the contour since it is perpendicular to the contour lines. On the other hand, the gradient in the direction of $\beta_1$ is zero as it is parallel with the contour line at $x_0$. Recall that contour lines show the points with the same value. When the points have the same values, the gradient is zero. We can then substitute this into the above equation. - -$$\hat{\beta}_0 = \hat{\beta}_0 - \alpha \frac{\partial}{\partial \hat{\beta}_0} J$$ - -$$\hat{\beta}_1 = \hat{\beta}_1 - \alpha \frac{\partial}{\partial \hat{\beta}_1} J$$ - -The partial derivative with respect to $\beta_0$ is non-zero while the derivative with respect to $\beta_1$ is zero. So we have the following: - -$$\hat{\beta}_0 = \hat{\beta}_0 - \alpha \times m $$ - -$$\hat{\beta}_1 = \hat{\beta}_1 $$ - -where $m$ is the gradient in $\beta_0$ direction which is the partial derivative $\partial J/\partial\hat{\beta}_0$. If the optimum is a minima, then $m < 0$, a negative value. Now, we can see how the next point increases in $\beta_0$ direction but not in $\beta_1$ direction at $x_0$. - -$$\hat{\beta}_0 = \hat{\beta}_0 + \delta $$ - -$$\hat{\beta}_1 = \hat{\beta}_1 $$ - -When the gradient in the $\beta_1$ direction is no longer zero as in the subsequent steps, both $\beta_0$ and $\beta_1$ increases by some amount. - -$$\hat{\beta}_0 = \hat{\beta}_0 + \delta_0 $$ - -$$\hat{\beta}_1 = \hat{\beta}_1 + \delta_1 $$ - -We can actually calculate the derivative of the cost function analytically. - -$$J(\hat{\beta}_0, \hat{\beta}_1) = \frac{1}{2m}\Sigma_{i=1}^m\left(\hat{y}(x^i) - y^i\right)^2$$ - -$$\frac{\partial}{\partial \hat{\beta}_j} J(\hat{\beta}_0, \hat{\beta}_1) = \frac{\partial}{\partial \hat{\beta}_j} \frac{1}{2m}\Sigma_{i=1}^m\left(\hat{y}(x^i) - y^i\right)^2$$ - -We can substitute our straight line equation into $\hat{y}$ to give the following. - -$$\frac{\partial}{\partial \hat{\beta}_j} J(\hat{\beta}_0, \hat{\beta}_1) = \frac{\partial}{\partial \hat{\beta}_j} \frac{1}{2m}\Sigma_{i=1}^m\left(\hat{\beta}_0 + \hat{\beta}_1 x^i - y^i\right)^2$$ - -Now we will differentiate the above equation with respect to $\hat{\beta}_0$ and $\hat{\beta}_1$. - -Let's first do it for $\hat{\beta}_0$. - -$$\frac{\partial}{\partial \hat{\beta}_0} J(\hat{\beta}_0, \hat{\beta}_1) = \frac{1}{m}\Sigma_{i=1}^m\left(\hat{\beta}_0 + \hat{\beta}_1 x^i - y^i\right)$$ - -or - -$$\frac{\partial}{\partial \hat{\beta}_0} J(\hat{\beta}_0, \hat{\beta}_1) = \frac{1}{m}\Sigma_{i=1}^m\left(\hat{y}(x^i) - y^i\right)$$ - -Now, we need to do the same by differentiating it with respect to $\hat{\beta}_1$. - -$$\frac{\partial}{\partial \hat{\beta}_1} J(\hat{\beta}_0, \hat{\beta}_1) = \frac{1}{m}\Sigma_{i=1}^m\left(\hat{\beta}_0 + \hat{\beta}_1 x^i - y^i\right) x^i$$ - -or - -$$\frac{\partial}{\partial \hat{\beta}_1} J(\hat{\beta}_0, \hat{\beta}_1) = \frac{1}{m}\Sigma_{i=1}^m\left(\hat{y}(x^i) - y^i\right) x^i$$ - -Now we have the equation to calculate the next values of $\hat{\beta}_0$ and $\hat{\beta}_1$ using gradient descent. - -$$\hat{\beta}_0 = \hat{\beta}_0 - \alpha \frac{1}{m}\Sigma_{i=1}^m\left(\hat{y}(x^i) - y^i\right)$$ - -$$\hat{\beta}_1 = \hat{\beta}_1 - \alpha \frac{1}{m}\Sigma_{i=1}^m\left(\hat{y}(x^i) - y^i\right)x^i$$ - -## Matrix Operations - -We can calculate these operations using matrix calculations. - -### Hypothesis - -Recall that our hypothesis (predicted value) for one data point was written as follows. - -$$\hat{y}(x^i) = \hat{\beta}_0 + \hat{\beta}_1 x^i$$ - -If we have $m$ data points, we will then have a set of equations. - -$$\hat{y}(x^1) = \hat{\beta}_0 + \hat{\beta}_1 x^1$$ -$$\hat{y}(x^2) = \hat{\beta}_0 + \hat{\beta}_1 x^2$$ -$$\ldots$$ -$$\hat{y}(x^m) = \hat{\beta}_0 + \hat{\beta}_1 x^m$$ - -We can rewrite this in terms of matrix multiplication. First, we write our independent variable $x$ as a column vector. - -$$\begin{bmatrix} -x^1\\ -x^2\\ -\ldots\\ -x^m -\end{bmatrix}$$ - -To write the system equations, we need to add a column of constant 1s into our independent column vector. - -$$\mathbf{X} = \begin{bmatrix} -1 & x^1\\ -1 & x^2\\ -\ldots & \ldots\\ -1 &x^m -\end{bmatrix}$$ - -and our constants as a column vector too. - -$$\mathbf{\hat{b}} = \begin{bmatrix} -\hat{\beta}_0\\ -\hat{\beta}_1 -\end{bmatrix}$$ - - - -Our system equations can then be written as - -$$\mathbf{\hat{y}} = \mathbf{X} \times \mathbf{\hat{b}}$$ - -The result of this matrix multiplication is a column vector of $m\times 1$. Note that in the above matrix equation, we use $\mathbf{\hat{y}}$ to denote the column vector of *predicted* value or our hypothesis. - -### Cost Function - -Recall that the cost function is written as follows. - -$$J(\hat{\beta}_0, \hat{\beta}_1) = \frac{1}{2m}\Sigma^m_{i=1}\left(\hat{y}(x^i)-y^i\right)^2$$ - -We can rewrite the square as a multiplication instead and make use of matrix multplication to express it. - -$$J(\hat{\beta}_0, \hat{\beta}_1) = \frac{1}{2m}\Sigma^m_{i=1}\left(\hat{y}(x^i)-y^i\right)\times \left(\hat{y}(x^i)-y^i\right)$$ - -Writing it as matrix multiplication gives us the following. - -$$J(\hat{\beta}_0, \hat{\beta}_1) = \frac{1}{2m}(\mathbf{\hat{y}}-\mathbf{y})^T\times (\mathbf{\hat{y}}-\mathbf{y})$$ - -### Gradient Descent - -Recall that our gradient descent equations update functions were written as follows. - -$$\hat{\beta}_0 = \hat{\beta}_0 - \alpha \frac{1}{m}\Sigma_{i=1}^m\left(\hat{y}(x^i) - y^i\right)$$ - -$$\hat{\beta}_1 = \hat{\beta}_1 - \alpha \frac{1}{m}\Sigma_{i=1}^m\left(\hat{y}(x^i) - y^i\right)x^i$$ - -And recall that our independent variable is a column vector with constant 1 appended into the first column. - -$$\mathbf{X} = \begin{bmatrix} -1 & x^1\\ -1 & x^2\\ -\ldots & \ldots\\ -1 &x^m -\end{bmatrix}$$ - -Transposing this column vector results in - -$$\mathbf{X}^T = \begin{bmatrix} -1 & 1 & \ldots & 1\\ -x^1 & x^2 & \ldots & x^m\\ -\end{bmatrix}$$ - - -Note that we can write the update function summation also as a matrix operations. - -$$\mathbf{\hat{b}} = \mathbf{\hat{b}} - \alpha \frac{1}{m}\mathbf{X}^T \times (\mathbf{\hat{y}} - \mathbf{y})$$ - -Substituting the equation for $\mathbf{\hat{y}}$, we get the following equation. - -$$\mathbf{\hat{b}} = \mathbf{\hat{b}} - \alpha \frac{1}{m}\mathbf{X}^T \times (\mathbf{X} \times \mathbf{\hat{b}} - \mathbf{y})$$ - -In this notation, the capital letter notation indicates matrices and small letter notation indicates vector. Those without bold notation are constants. - -## Metrics - -After we build our model, we usually want to evaluate how good our model is. We use metrics to evaluate our model or hypothesis. To do this, we should split the data into two: -- training data set -- test data set - -The training data set is used to build the model or the hypothesis. The test data set, on the other hand, is used to evaluate the model by computing some metrics. - - -### Mean Squared Error - -One metric we can use here is called the mean squared error. The mean squred error is computed as follows. - -$$MSE = \frac{1}{n}\Sigma_{i=1}^n(y^i - \hat{y}^i)^2$$ - -where $n$ is the number of predicted data points in the *test* data set, $y^i$ is the actual value in the *test* data set, and $\hat{y}^i$ is the predicted value obtained using the hypothesis and the independent variable $x^i$ in the *test* data set. - -### R2 Coefficient of Determination - -Another metric is called the $r^2$ coefficient or the coefficient of determination. This is computed as follows. - -$$r^2 = 1 - \frac{SS_{res}}{SS_{tot}}$$ - -where - -$$SS_{res} = \Sigma_{i=1}^n (y_i - \hat{y}_i)^2$$ where $y_i$ is the actual target value and $\hat{y}_i$ is the predicted target value. - -$$SS_{tot} = \Sigma_{i=1}^n (y_i - \overline{y})^2$$ - -where -$$ \overline{y} = \frac{1}{n} \Sigma_{i=1}^n y_i$$ -and $n$ is the number of target values. - -This coefficient gives you how close the data is to a straight line. The closer it is to a straight line, the value will be close to 1.0. This means there is a correlation between the independent variable and the dependent variable. When there is no correlation, the value will be close to 0. - -In your problem sets, you will work on writing a code to do all these tasks. diff --git a/_Notes/Linear_Data_Structures.md b/_Notes/Linear_Data_Structures.md deleted file mode 100644 index 118164a..0000000 --- a/_Notes/Linear_Data_Structures.md +++ /dev/null @@ -1,302 +0,0 @@ ---- -title: Linear Data Structures -permalink: /notes/linear_data_structure -key: notes-linear-data-structure -layout: article -nav_key: Notes -sidebar: - nav: Notes -license: false -aside: - toc: true -show_edit_on_github: false -show_date: false ---- - -## Introduction - -You have encountered `list` as one of the built-in data types that Python support. You can use list to represent one dimensional or linear data collection where sequence and order matters. There are other kinds of linear data structures and we will explore some of them in this lesson. - -## Stack - -Stack is a type of data structure that follows the LIFO (Last in First out) principle. Stack is common in daily life. Consider a **stack** of books. - -
- -Now, let's think about what are the operations we can do with such a structure. We can do the following: -- We can add new book into the stack by putting it at the top. This operation is called a **push**. -- Or you can remove a book from the stack by taking the one at the top. This operation is called a **pop**. -- Or you can simply look at the book at the top of the stack. This operation is called a **peek**. - -What you cannot do, however, are the following: -- insert a book somewhere in the middle of the stack -- take out a book from somewhere in the middle of the stack - -If you want to do those two operations, you have to start by removing the books at the top first. This is why Stack is called Last in First out (LIFO). We can generalize this into an abstract concept of Stack data structure as shown below. - -
- -As you can see, there are three operations related to Stack: - -- push -- pop -- and peek - -We can create a stack using Object Oriented Programming by defining a class. A Stack class has at least the following attributes and methods. - - -``` -Stack -Attributes: -1. items - -Methods: -1. Push // insert -2. Pop // remove and read -3. Peek // only read -``` - - -You can use [this animation](https://yongdanielliang.github.io/animation/web/Stack.html) to get familiar with the Stack operation. - -## Queue - -Queue is another common data structure that we find frequently in daily life. For example, the image below shows a queue of people to Louvre Museum. -
- - -Notice that Queue is different from Stack. Stack follows Last in First out principle. On the other hand, Queue follows the First in First out (FIFO) principle. The first person that enters the queue is the first person that can enter the Louvre Museum. We can abstract this as a kind of data structures shown below. - - -
- - -Queues are similar to Stacks in some manners. For example, you can't access the elements in middle of the queue. This is like taking someone from the middle of the queue and let him or her enter the museum before those who are at the front of the queue. You can't also insert an element to somewhere in the middle of the queue. This is called cutting queues. No one will be happy with this. So we can only access item from the front of the queue and insert an item from the rear of the queue. These are the two operations of Queues. - -- `enqueue` is to put an item from the rear of the queue, -- `dequeue` is to take an item out from the front of the queue, -- and `peek` is similar to Stack operation which is just to read the item at the front of the queue without removing it from the queue. - -As such a Queue data structure must have at least the following attributes and methods: - -``` -Queue -Attributes: -1. items - -Methods: -1. Enqueue -2. Dequeue -3. Peek -``` - -You can use [this animation](https://yongdanielliang.github.io/animation/web/Queue.html) to get familiar with Queue operations. - -## Applications - -How or when do we use such data structures like Stack and Queue. In this section we will illustrate two examples. The first one is an example of Stack's application and the second one is an example for Queue's application. - -### Post-Fix Expression Evaluation - -One application that uses Stack data structure is to evaluate a Post-Fix Expression. Our mathematical expression is normally expressed as an *infix* notation. For example, - -$$3 + 4 \times 2$$ - -In this notation, $4 \times 2$ is evaluated first and the result is added to 3 to get the final result. In this notation, the operators are *in between* the operands. This is why it is called *infix* notation. But, this is not the only notation. We can represent the same mathematical operations using a **Post-Fix** notation. In this notation, the operators are placed before the operands justyifing its name, i.e. *postfix*. Let's write the same mathematical expression using a post-fix notation. - -$$4 2 \times 3 +$$ - -In the above notation, we can see that the operators are placed after the operands. The first two numbers are the operands. The third one is an operator for multiplication. So we will multiply the first two numbers 4 and 2. The next one is another number, i.e. 3. And the last one is an addition operator. This means that we will add 3 with the result of the multiplication of 4 and 2. - -How do we use Stack to evaluate post-fix notation? The steps are written below. - -``` -Post-Fix Evaluation Steps: -1. Read the expression from left to right. -2. If it is an operand (not an operator symbol), do the following: - 2.1. put the operand into the stack. -3. Otherwise (this is an operator), do the following: - 3.1. pop out the top of the stack as the *right* operand - 3.1. pop out the top of the stack as the *left* operand - 3.1. evaluate the operator with the operands - 3.1. push the result into the stack -``` - -### Program's Stack - -In fact, perhaps unknowingly, you have had an encounter with stacks just a week ago. Computer actually uses stacks in recursion! (Call stacks are an important concept in general programming too!) Let's look at this in action with the factorial fuction. factorial(3), or 3! = 3\*2\*1. Here's a recursive function to calculate the factorial of a number: - -```python -def factorial(x): - if x == 1: - return 1 - else: - return x * factorial(x-1) - -print(factorial(3)) -``` - -[You can visualize the Stack calls using Python Tutor](http://pythontutor.com/visualize.html#code=def%20factorial%28x%29%3A%0A%20%20if%20x%20%3D%3D%201%3A%0A%20%20%20%20return%201%0A%20%20else%3A%0A%20%20%20%20return%20x%20*%20factorial%28x-1%29%0A%0Aprint%28factorial%283%29%29&cumulative=false&curInstr=9&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=3&rawInputLstJSON=%5B%5D&textReferences=false). Notice that the computer treats the frames for factorial in a way like Stack operations that grows downward. - -As mentioned, stacks are an important concept on how computer works. You may wonder why the name most voted for the most popular website for programmers is called [**Stack** Overflow](https://meta.stackoverflow.com/questions/266557/why-was-stack-overflow-chosen-as-a-name-for-this-site#:~:text=overflow%22%20error%20occurs.-,Thus%2C%20naming%20the%20site%20Stack%20Overflow%20is%20a%20bit%20of,Share%20and%20enjoy.). - -### Radix Sort with Queue - -We have seen how Stack is used to evaluate post-fix notation. Now, we will work with another algorithm called Radix sort to show how Queue can be used. Radix sort is a non-comparison sorting algorithm for **integers** by grouping integers by individual digits that share the same positon and value. It utilizes 10 "buckets" numbered from 0-9 to sort. - -Radix sort will go through each digit of all numbers and put them in the buckets matching their digit, and take them out again, repeating until all digits are checked. - -A simple animation for radix sorting: -
- -
- -Source: [visualgo.net](https://visualgo.net/en/sorting?slide=16). -(Visualgo provides a lot of nice animations of many different algorithms that may help you visualize the algorithm better) - -There are two kinds of Queues used in Radix sort: -1. 1 x Main bin queue -1. 10 x Radix bin queues - -The Radix sort operation can be described as follows: -1. First, put all the item into the Main bin queue. -1. The next step is to start with the lowest digit. In this case, it is the *ones* digit. We take out all the items from the Main bin and put it into the respective radix bins. If the ones is 0, we put into radix bin 0. If the ones is 1, we put into the radix bin 1. If the ones is 2, we put into the radix bin 2, and so on until 9. -1. Once we finish putting all the items into the respective radix bins, we empty out the radix bin queue and put the items back into the Main bin queue. We start from radix 0 and continue until radix bin 9. -1. We repeat this step until we reach the highest digits. - -Let's give some example using four numbers: 101, 21, 4000, 7. We can rewrite these numbers up to four digits: - -``` -0101, 0021, 4000, and 0007. -``` - -We can then start from the lowest digit, the ones. As we take out the items from the Main bin queue, we do the following: -1. put `010(1)` into radix bin 1 -1. put `002(1)` into radix bin 1 -1. put `400(0)` into radix bin 0 -1. put `000(7)` into radix bin 7 - -The radix bin will be filled as follows: -- Bin 0: 4000 -- Bin 1: 0101, 0021 -- Bin 2: -- Bin 3: -- Bin 4: -- Bin 5: -- Bin 6: -- Bin 7: 0007 -- Bin 8: -- Bin 9: - -Once we are done, we will take out the items from the radix bins and put it back into the Main bin queue. We do this starting from radix bin 0. The main queue now contains. - -``` -4000, 0101, 0021, 0007 -``` - -Now, we are ready to do with the tens digit. We take out from the main bin queue to the radix bins. -1. put `40(0)0` into radix bin 0 -1. put `01(0)1` into radix bin 0 -1. put `00(2)1` into radix bin 2 -1. put `00(0)7` into radix bin 0 - -The radix bin will be filled as follows: -- Bin 0: 4000, 0101, 0007 -- Bin 1: -- Bin 2: 0021 -- Bin 3: -- Bin 4: -- Bin 5: -- Bin 6: -- Bin 7: -- Bin 8: -- Bin 9: - -Now, we will put back into the Main bin queue. - -``` -4000, 0101, 0007, 0021 -``` - -We repeat again the steps for the hundreds. -1. put `4(0)00` into radix bin 0 -1. put `0(1)01` into radix bin 1 -1. put `0(0)07` into radix bin 0 -1. put `0(0)21` into radix bin 0 - -The state of the radix bin will be as follows. -- Bin 0: 4000, 0007, 0021 -- Bin 1: 0101 -- Bin 2: -- Bin 3: -- Bin 4: -- Bin 5: -- Bin 6: -- Bin 7: -- Bin 8: -- Bin 9: - -Lastly, we do the same steps for the thousands digit. -1. we put `(4)000` into radix bin 4 -1. we put `(0)007` into radix bin 0 -1. we put `(0)021` into radix bin 0 -1. we put `(0)101` into radix bin 0 - -And the state of the radix bin will be as follows. -- Bin 0: 0007, 0021, 0101 -- Bin 1: -- Bin 2: -- Bin 3: -- Bin 4: 4000 -- Bin 5: -- Bin 6: -- Bin 7: -- Bin 8: -- Bin 9: - -After we take out and put into the Main bin, we will have - -``` -0007, 0021, 0101, and 4000 -``` -or -``` -7, 21, 101, 4000 -``` - -which is the sorted arrangement of the numbers. - -## Queue with Double Stack - -Queue data structure can be implemented in different ways. The first way that comes to our mind maybe simply to use a list as its internal storage. The problem with list is that one of the Queue operation will be slow. Why is this so? Consider if we use the following code to add item into the list: -```python -def enqueue(self, item): - self.items.append(item) -``` -In this example, whenever we add item into the Queue, we always add it to the back. This operation takes constant time $O(1)$. However, the removal part, must be written as - -```python -def dequeue(self): - return self.items.pop(0) -``` - -The problem with this implementation is that it is very slow. The reason is that Python has to move all the elements after index 0 one position to the left. This takes $O(n)$ where $n$ is the number of item in the Queue. This motivates us to think whether there is any other way of implementing Queue. - -The answer is yes. We can use 2 Stack data structures as Queue's internal storage. In this implementation, we have two stacks: -- Left Stack -- Right Stack - -An example of a Queue implemented using 2 Stacks are shown below. - -
- -With this implementation both enqueue and dequeue are constant time $O(1)$. Recall that it takes constant time to add an item to the end of a list and to pop an item from the end of a list. What is tricky about this implementation is that when we try to dequeue an item while the Left Stack is empty. To do this we follow the following procedures: -1. Copy all items from the Right Stack to the Left Stack. -1. Reverse the items in the Left Stack. -1. Remove the items on the Right Stack. -1. Pop the requested item from the Left Stack. - -These steps are shown in the image below. - -
- diff --git a/_Notes/List.md b/_Notes/List.md new file mode 100755 index 0000000..64b1b96 --- /dev/null +++ b/_Notes/List.md @@ -0,0 +1,1323 @@ +--- +title: List +permalink: /notes/list +key: notes-list +layout: article +nav_key: Notes +sidebar: + nav: Notes +license: false +aside: + toc: true +show_edit_on_github: false +show_date: false +--- + +## What is List? +In the previous lesson, we introduced an immutable collection data type called Tuple. In this lesson, we will work with List. The main difference between Tuple and List is that List is mutable. This means that you can modify its elements. You can also append, insert or remove the elements of a List which you cannot do with a tuple. However, there are similarities also between a tuple and a list. Both are a collection of items that may have different types. Though List can have elements with different types, it is more common to use list for homogenous element types. Tuple is more frequently used if the elements are of different type. Another similarity is that the elements of both tuple and list can be accessed using an index of integer type. + +List is more commonly used than tuple in many cases because of its mutability. At the same time, there are cases where we need to be aware on how list works especially when we assign list or pass list to a function. We will discuss all this in this lesson. + +## Creating a List + +Let's begin by creating a list. Previously, we created a tuple by assigning a number of items into a variable separated by a comma. The use of the parenthesis, i.e. `()`, was optional to create a tuple. List data type is created also by assembling items separated by a comma. However, to indicate to Python that we want to create a list instead of a tuple, we use a **square bracket** when creating a list literal, i.e. `[]`. + +```python +>>> my_pies: list = [3, 3.14, '3.14'] +>>> print(my_pies) +[3, 3.14, '3.14'] +>>> type(my_pies) + +``` + +As we mentioned in the first section, though List can have elements of different types, it is more common to create list where the elements are of the same type. In this case, we can also use type annotation when creating a list where the elements are of the same type. + +```python +my_steps: list[int] = [40, 50, 43] +print(my_steps) +print(type(my_steps)) +``` + +In the above code we annotate `my_steps` as follows + +```python +my_steps: list[int] +``` + +## Basic Operations for List + +### Accessing an Element in a List + +Once we know how to create a List, we can now show some of the common basic operations with List data type. The most basic one is to access the element in a list. The way we do it is exactly the same as accessing an element in a Tuple. We use the get item operator which is a square bracket operator with an index. + +```python +>>> my_steps: list[int] = [40, 50, 43] +>>> print(my_steps[0]) +40 +>>> print(my_steps[2]) +43 +>>> print(my_steps[-2]) +50 +``` + +Notice that we can access the first element from index 0 and the last element from size of list minus 1, i.e. `len(my_steps) - 1`. We can also use the negative index where -1 refers to the last element. In the example above, -2 index will refer to the second last element, which is 50. + +### Checking If an Element is in a List + +So we know how to read a value in a list. There are many cases when we are only interested to know if a certain item is in the list. Is it in the list or not? If it is maybe we will do some processing but if it is not, we may do something else. Python provides `in` operator to check if an object is in the list. + +```python +>>> my_steps: list[int] = [40, 50, 43] +>>> 40 in my_steps +True +>>> 43 in my_steps +True +>>> 53 in my_steps +False +``` + +Usually, we use the `in` operator with the if-statement. + +```python +if item in list: + do_something() +else: + do_something_else() +``` + +### Modifying an Element in a List + +The get item operator can be used together with the assignment operator `=` to change an element in the list. + +```python +>>> my_steps: list[int] = [40, 50, 43] +>>> print(my_steps[0]) +40 +>>> my_steps[0] = 65 +>>> print(my_steps[0]) +65 +``` + +In the above code, we modify the first element (index 0) from 40 to 65. + +### Getting the Number of Elements in a List + +We can get the length of a list from the `len()` built-in function and so we can access the last element using this. + +```python +>>> my_steps: list[int] = [40, 50, 43] +>>> print(my_steps[len(my_steps) - 1]) +43 +``` + +Notice that the last element is the size of the list minus one. Therefore, if we use the size of the list as an index to access an element, this results in trying to access an element that is outside of the range. + +```python +>>> my_steps: list[int] = [40, 50, 43] +>>> print(my_steps[len(my_steps)]) +Traceback (most recent call last): + File "", line 1, in +IndexError: list index out of range +``` + +The error message indicates that the list index is out of range. This is because the last index is `len(my_steps) - 1`. + +### Adding an Element Into a List + +Another basic operations is to add elements into a list. There are multiple ways of adding an element into a list. The first one is to add an element at the **back** of the list. This is called **appending**. Since list is an *ordered* linear data structure, there is a sequence in the element. We can talk about the first item and the last item in a list. Appending is adding the item at the end of the list as the last item of the list. As the name suggest, the method to do this is `list.append(item)`. + +```python +>>> my_steps: list[int] = [40, 50, 43] +>>> my_steps.append(52) +>>> my_steps +[40, 50, 43, 52] +``` + +In the above code, we add 52 at the end of the list. What if we want to add item at the first position in the list? In this case, we can use the `list.insert(pos, item)` method. + +```python +>>> my_steps +[40, 50, 43, 52] +>>> my_steps.insert(0, 45) +>>> my_steps +[45, 40, 50, 43, 52] +``` + +The method `insert()` has two arguments. The first argument specifies at which position you want to insert the element and the second argument is the element you want to insert. In the above example, we inserted 45 into position 0, which is the first position in the list. We can insert element into the other position as well. + +```python +TypeError: 'list' object is not callable +>>> my_steps +[45, 40, 50, 43, 52] +>>> my_steps.insert(2, 65) +>>> my_steps +[45, 40, 65, 50, 43, 52] +``` + +In the above code, we inserted 65 into the third position (index 2). + +One important thing to note is that both `.append()` and `.insert()` does not return any value. These methods modify the list object that is attached to these methods. Notice that we call the method in this way. + +```python +object_name.append(item) +``` + +In our case above, the object name was `my_steps`. We then use the "dot" operator to access the method `append()` of this object. In this case, this method is attached to a list object called `my_steps`. The dot operator in `my_steps.append(52)` can be read as follows: "call the append() method OF my_steps object with an input argument 52". The append() belongs to a list object `my_steps`. In fact, every list object that you create has this `append()` method. What this method does is simply appending an item to the object it belongs to which in this case is `my_steps`. However, this method has no return value. We can check that it has no return value by storing the output of this method to a variable and print the variable value as shown below. + +```python +>>> output = my_steps.append(70) +>>> print(output) +None +>>> print(my_steps) +[45, 40, 65, 50, 43, 52, 70] +``` + +We can see that the output of `print(output)` is `None`. However, when you print the list object `my_steps`, you can see that it has been modified with the new element at the end. + +Another way of adding elements into a list is by concatenating two lists or extending a list with another list. The operator for concatenating two objects is the `+` operator which we have used for concatenating two strings. We can use the same operator for concatenating two lists. + +```python +>>> week1_steps: list[int] = [40, 45, 50] +>>> week2_steps: list[int] = [55, 52, 60] +>>> week1_and_2: list[int] = week1_steps + week2_steps +>>> week1_and_2 +[40, 45, 50, 55, 52, 60] +``` + +We can also use the `list1.extend(list2)` method to extend a list with another list. The second list is extended to the back of the first list. + +```python +>>> week1_steps +[40, 45, 50] +>>> week2_steps +[55, 52, 60] +>>> week1_steps.extend(week2_steps) +>>> week1_steps +[40, 45, 50, 55, 52, 60] +``` + +There is a subtle difference between using `+` operator and `.extend()` method. When using the `+` operator, a new list containing both lists are created. This new concatenated list is different from both the first and the second list. However, when we use the `.extend()` method. This method is similar to `.append()` and `.insert()` in the sense that it modifies the object that is attached to it through the dot operator and does not return any other value. We can see from the above example that the list object `week1_steps` is changed to contain the second list. + +### Removing an Element From a List + +We have shown how to access an element in a list and how to add elements into the list. Now, we can show how to remove elements from a list. The common operator to remove elements from a list is the `del` operator. We use it in this way. + +```python +del my_list[index] +``` +where the index is the position of the item we want to remove. For example, we can remove the second element of a list using the following. + +```python +>>> my_steps +[45, 40, 65, 50, 43, 52, 70] +>>> del my_steps[1] +>>> my_steps +[45, 65, 50, 43, 52, 70] +``` + +Notice that the second element, which is indexed by 1, has been removed from the list, i.e. 40. However, there are times that we know the item value that we want to remove rather than its index. A convinient method to remove an item in this case is to use the `list.remove(item)` method. For example, if we want to remove 43 from the list above, we can type the following. + +```python +>>> my_steps +[45, 65, 50, 43, 52, 70] +>>> my_steps.remove(43) +>>> my_steps +[45, 65, 50, 52, 70] +``` + +This `.remove()` method is similar to the other list methods in the sense that it does not return any value. However, these methods modify the existing list which in this case is `my_steps`. We can see that `my_steps` no longer contains 43 after calling `my_steps.remove(43)`. + +Just as it is common to add items from the back of the list, it is also common to remove items from the back of the list. This is typically true when list is used as stack data structure. We will discuss stack at some other lessons but for now we can think of stack data structure similar to a stack of books on a table where we can put book at the top of the stack and remove the book only from the top of the stack. Python provides `list.pop()` method for this. + +```python +>>> my_steps +[45, 65, 50, 52, 70] +>>> out: int = my_steps.pop() +>>> print(out) +70 +>>> my_steps +[45, 65, 50, 52] +``` + +Notice above that `pop()` method does two things: +* first, it returns the last element as the output of the `pop()` method. +* second, it removes the last element from the list and thereby changing the list itself. + +In the above example, 70 was removed from `my_steps` and it is returned by `pop()` to be assigned to `out` variable. + +What is interesting is that you can change the behaviour of `pop()` by providing an argument. Instead of popping the element from the last item of the list, you can specify which item you want to pop out from the list. For example, let's say if we want to pop out the first item of the list, we can type the following code. + +```python +>>> my_steps +[45, 65, 50, 52] +>>> out: int = my_steps.pop(0) +>>> print(out) +45 +>>> print(my_steps) +[65, 50, 52] +``` + +You can see that 45, which is the first element, is removed from the list and is stored into `out`. How different `pop()` is with `del` keyword. Both seems to remove the element by providing its index. The difference lies in the fact that `pop()` returns the element that is removed from the list while `del` does not. In fact, if you try to assign to a variable when using `del` it will give an error. + +```python +>>> out = del my_steps[0] + File "", line 1 + out = del my_steps[0] + ^ +SyntaxError: invalid syntax +``` + +The error says it is an invalid syntax because `del` keyword is expected to be the first token in a statement and cannot be used with an assignment operator. + +### Finding the Index of an Element + +What if we want to know the position of a particular item in the list? Can we find its index? Python provides `index()` method to find the index of a particular element. + +```python +>>> my_steps +[65, 50, 52] +>>> my_steps.index(52) +2 +``` +In the above example, `my_steps.index(52)` finds the index of an element 52 in the list `my_steps`. What if there are more than one elements in the list? Can we find its second occurance position? We can still use the same `index()` method by providing the second argument. The second argument specifies from which position you want to start finding the element. See example below. First, we will add another element with the same value 52 and then we will find the two positions in the list. + +```python +>>> my_steps +[65, 50, 52] +>>> my_steps.append(52) +>>> my_steps +[65, 50, 52, 52] +>>> first_pos: int = my_steps.index(52) +>>> first_pos +2 +>>> second_pos: int = my_steps.index(52, first_pos + 1) +>>> second_pos +3 +``` + +In the above code, we first append another 52 into the list `my_steps`. Then, we capture the first position of 52 by using the `index(52)` method. We stored this output into `first_pos` variable. We then used this position to find the second position by providing it into the second argument of `index()`. We put `first_pos + 1` because we want to start find the next 52. + +## Creating a New List From an Existing Data + +We have learnt how to create a list using what we call as a *list literal*. We used square bracket to create a new list. + +```python +my_steps: list[int] = [40, 50, 43, 55, 67, 56, 60] +``` + +We can also create a new list from any existing data. In this section, we will discuss some of a few way to create a new list from an existing data. + +### List Conversion Function + +Python provides a built-in function `list()` to convert any other data type to a list. This means that we can convert a tuple into a list or a string into a list. See code below. + +```python +step_as_tuple: tuple[str, int] = ('John', 50) +step_as_list: list[str, int] = list(step_as_tuple) +print(step_as_list, type(step_as_list)) +``` + +The output displays the following. +``` +['John', 50] +``` + +Similarly, we can convert a string into a list. In this case, every character will be an individual element in the converted list. + +```python +name: str = 'John' +name_as_list: list[str] = list(name) +print(name_as_list) +``` + +The output is shown below. +``` +['J', 'o', 'h', 'n'] +``` + +### Copying a List into a New List + +We can also make a copy of an existing list into a new list. To do this, we will use the `copy()` function from the `copy` library. + +```python +import copy +my_steps: list[int] = [40, 50, 43, 55, 67, 56, 60] +my_steps_backup: list[int] = copy.copy(my_steps) +print(my_steps, id(my_steps)) +print(my_steps_backup, id(my_steps_backup)) +``` + +The output is shown below. + +``` +[40, 50, 43, 55, 67, 56, 60] 4454460608 +[40, 50, 43, 55, 67, 56, 60] 4454499840 +``` + +In the above code, we used `id()` to check the object id in the memory. We can see the output on the second number after the list. Notice that the two ids are different because they are two different objects in the memory. The `copy()` function has created a new object with the same value as the first list `my_steps`. + +### Slicing a List + +Another way we can create a new list is by *slicing* the list. Python provides a convenient syntax using the square bracket operator (or the get item operator). The format is the following. + +``` +my_list[start:end:step] +``` + +One thing that we have to remember is that the **end index** is exclusive. This means that it excludes the element pointed by the *end index*. Let's illustrate this with an example. Let's start with our existing list. + +```python +my_steps: list[int] = [40, 50, 43, 55, 67, 56, 60] +``` + +We can create a new list consisting of `[50, 43, 55]` which is the second element to the fourth element in the list above using slicing. +```python +mon_to_wed_steps: list[int] = my_steps[1:4] +print(mon_to_wed_steps) +``` + +The output is shown below. +``` +[50, 43, 55] +``` + +Notice that the index starts from 0 as shown in the table below. + +| positive index | 0 | 1 | 2 | 3 | 4 | 5 | 6 | +|----------------|----|----|----|----|----|----|----| +| element | 40 | 50 | 43 | 55 | 67 | 56 | 60 | + +Notice that we did not specify the steps in the slicing arguments. By default the step size is 1. The `start` and the `end` index also has its default values. By default `start` is 0. This means that you don't need to specify the start index if you want to slice from the first element. + +```python +sun_to_wed: list[int] = my_steps[:4] +print(sun_to_wed) +``` + +The output is shown below. + +``` +[40, 50, 43, 55] +``` + +The default for the `end` index, however, is **one plus** the last element's index. In our case, the last element is 6, so the default for the end index is 7. Therefore, these two codes are equivalent. + +```python +copy_1: list[int] = my_steps[0:7] +copy_2: list[int] = my_steps[:] +print(copy_1, id(copy_1)) +print(copy_2, id(copy_2)) +``` + +The output is shown below. + +``` +[40, 50, 43, 55, 67, 56, 60] 4405092864 +[40, 50, 43, 55, 67, 56, 60] 4405094080 +``` + +Notice that we have just created a duplicate of an existing list using slicing similar to what we do with `copy()`. + +We can also slice the list using a negative index. See table below. + +| negative index | -7 | -6 | -5 | -4 | -3 | -2 | -1 | +|----------------|----|----|----|----|----|----|----| +| element | 40 | 50 | 43 | 55 | 67 | 56 | 60 | + +Notice the difference between the positive indexing and the negative index. While the positive indexing starts from 0, the negative indexing starts from -1. This is useful when we want to create a new list counting from the back. For example, let's say we want to get the *last three elements*. + +```python +last_three_days: list[int] = my_steps[-3:] +print(last_three_days) +``` + +The output is shown below. + +``` +[67, 56, 60] +``` + +Notice in the above slicing that we have used the default value for the `end` index. We can get the same results using the following code. + +```python +last_three_days: list[int] = my_steps[-3:7] +``` + +The above example shows that you can actually mixed positive and negative indexing. However, utilising the default values are useful because it is just more intuitive to retrieve the last *three days* with indexing `my_steps[-3:]`. + +We can also get the first three days using the positive indexing and default values as shown below. + +```python +first_three_days: list[int] = my_steps[:3] +print(first_three_days) +``` + +The output is shown below. + +``` +[40, 50, 43] +``` + +As can be seen above it is intuitive to get the first three days with slicing index of `my_steps[:3]`. This also explains why Python chose the `end` index is **exclusive**. The reason is that you easily get the number of elements from `start-end`. As in the example of the first three days, you know there are three days because $3-0=3$. Similarly, when we try to get the steps from Monday to Wednesday, the `end` index was 4 rather than 3 because $4-1=3$. This gives us a number of days when slicing `my_steps[1:4]`. + +Another way to see the slice index is to put them at the boundary of the elements as shown in the figure below. + + + +In the figure above, we can see the `start` and `end` index as the boundary of the sliced elements. For example, when we want to get the steps from the second element to the fourth, we can see that the boundary enclosing those elements is 1 on the left and 4 on the right. The same thing works for the negative index. + +The third argument in the get item operator for list slicing is the step size. We can specify the step size other than the default value 1. For example, let's day we want to get the data every two days. We can do so using the code below. + +```python +my_steps: list[int] = [40, 50, 43, 55, 67, 56, 60] +every_other_days: list[int] = my_steps[::2] +print(every_other_days) +``` + +The output is shown below. + +```python +[40, 43, 67, 60] +``` + +Notice that since we did not specify the `start` and `end` index, Python takes their default values 0 and 7 for these two arguments. The resulting slice starts from the first element to the last but with a step of 2. We can also specify a negative step size. This is useful for example if we want to get a new list with a reverse order. Let's say, we can get the last three days starting from the last day to the earlier day using the code below. + +```python +my_steps: list[int] = [40, 50, 43, 55, 67, 56, 60] +reverse_last_three_days: list[int] = my_steps[-1:-4:-1] +print(reverse_last_three_days) +``` +The output is shown below. + +```python +[60, 56, 67] +``` + +In the above slicing code, the `start` index was -1 which is the index of the last element using the negative indexing. The `end` index was -4 which is exclusive. Getting the difference between these two indices tells us that the resulting list will have 3 elements. The step size is specified as -1 because we start our indexing from the back and end it at the front. + +## Aliasing a List + +One important concept about list data structure in Python is the concept of alias. This has implication on how list is copied and when it is passed on to a function. To start with, let's recall the concept of variable in a primitive data type like an integer. + +```python +a: int = 4 +b: int = a +``` + +In Python, variable names are just label that is binded to some value. In this case, the integer value 4 is binded to a name `a`. When we do assignment such as `b = a` as in the code above, Python evaluates the value of `a` and get `4`. This value is then binded to the name `b`. In this way, changing `b` will not affect `a`. + +```python +a: int = 4 +b: int = a +b = -3 +print(a) +print(b) +``` + +The output is shown below. + +```python +4 +-3 +``` + +We can see the environment diagram from Python Tutor below. + + + +This is different in the case of list data type. + +```python +my_steps: list[int] = [40, 50, 40, 50] +copy_steps: list[int] = my_steps +copy_steps[0] = 75 +print(my_steps) +print(copy_steps) +``` + +The output is shown below. + +```python +[75, 50, 40, 50] +[75, 50, 40, 50] +``` + +Notice that both `my_steps` and `copy_steps` first elements are modified to 75. It's important to pause here and ponder what happens. In our code above, we created a list called `my_steps`. We then create another variable called `copy_steps` and assign `my_steps` to this variable. Unlike integer, the value that is binded to the new name `copy_steps` is a **reference** to the list **object**. This reference can be represented as an **arrow** in the environment diagram. Compare the environment diagram below with that for the integer case. + + + +Notice the arrow in that environment diagram that points to the same list object. Because the two names point to the same object, modifying one of them causes modification on the other name. The reason of this is that `b` is an **alias** to `a` and so points to the same object as `a`. This is different from that when we deal with integer data type. For the case of integer data type, the integer object is duplicated and `b` points to a different integer object (Python has some optimisation for small integers but that's for another story). + +The environment diagram before `b` is modified in the case of integer data type is shown below when the program counter is at line 3. + + + + +This aliasing effect of list data type becomes important when we pass list data into a function as one of the input arguments. + +## Passing a List into a Function as Input + +In the previous section, we mentioned that list behaves differently than integer data type when we assign a variable to an existing data. In the case of integer data type, the integer object is duplicated when we assign a different name. However, in the case of list, the list object is not duplicated but rather the two names point to the same object. What happens for these two data types when we pass them into a function as input arguments? + +Let's start again with an integer data type. We can pass an integer as an argument as shown below. + +```python +def compute_cadence_for_30sec(steps: int) -> int: + return steps * 2 + +my_step: int = 40 +cadence = compute_cadence_for_30sec(my_step) +``` + +Notice the environment diagram as we enter the function `compute_cadence_for_30sec`. + + + +In the environment diagram shown above, the argument `steps` is a variable in the local frame of the function `compute_cadence_for_30sec`. This `steps` has a value of 40 and is different from that of the variable `my_steps` in the *global* frame. The value 40 is duplicated and passed into the function. + +As you can guess, the behaviour is different in the case when we pass a list data type into a function. Let's modify the function to take in a list. + + + +We purposely stop the step in Python Tutor at line 4 where we just entered the function after the function call. Notice the environment diagram on the right. We can see that both `my_steps` and `list_steps` point to the same list object. + +See if you can understand the above code and what it does. We will show in the following section how to traverse a list. But for now, our interest is to show that when we pass a list as an input argument to a function, what is being passed on is the **reference** to this list. This means that you may **modify** the list inside the function. We should avoid this as it may be harder to debug your code. The reason is that when you modify the list inside the function, the list in the global frame is also modified. See the code below for an example. + + + +We have modified the code just to illustrate this aliasing effect inside a functin. Let's say you have a code to correct one of the element of the list. In the code above, we added the first step by 10. Notice that at the end of the code, both `my_steps` and `corrected_steps` have the same value on the first element, i.e. it was changed from 40 to 50. We do expect `corrected_steps` to be changed but not `my_steps`. To avoid this aliasing effect, we should have copied the list as shown below. + + + +Notice, in this case now `my_steps` and `corrected_steps` are two different list and we did not modify `my_steps` in the process. + +## Mutable and Immutable Data Type + +By now, you may wonder how do we know whether an object is duplicated or only its reference. It turns out that Python's data type falls into either **mutable** or **immutable** data type. Mutable means that it can be changed (Remember the mutant in X-men?). Immutable means that it cannot be changed. The following table list down the mutable and immutable data types. + +| immutable | mutable | +|-----------|---------------------| +| int | list | +| float | dictionary | +| str | set | +| tuple | user-defined object | + +You may be surprised to see that integer and float as immutable as it seems that we can actually modify integer as in the code below. + +```python +a: int = 4 +a = 5 +``` + +However, we **didn't** change the integer object `4`. What happens is that we create a new integer object `5` and bind it to the same name `a`. The integer object `4` itself is immutable. This is more obvious in a string when you try to modify one of the character in a string. Recall that we can access a character in a string using the get item operator. + +```python +name: str = "John Wick" +print(name[0]) +``` + +This outputs `J`. However, we are not able to modify this element. + +```python +name: str = "John Wick" +name[0] = "B" +``` + +It will output the following error message. + +``` +Traceback (most recent call last): + File "", line 1, in +TypeError: 'str' object does not support item assignment +``` + +String object is immutable and so it does not support item assignment. Similarly with tuple. + +```python +contact_info: tuple = ("John Wick", 81234567, "john@wick.com") +contact_info[1] = 91234567 +``` + +This also gives an error as shown below. + +``` +Traceback (most recent call last): + File "", line 1, in +TypeError: 'tuple' object does not support item assignment +``` + +This is not the case for mutable data type such as list. We have shown that we can actually modify an element in a list. + +```python +my_steps: list[int] = [40, 50, 43] +my_steps[0] = 65 +``` + +The above code does not produce error and it actually modifies the first element to 65. + +We will discuss the other data type such as dictionary and set on the subsequent sections. One important thing to note for now is that these mutable data type binds its *reference* to its object to a variable name. This is the arrow that we have been seeing in the Python Tutor visualisation previously. When we copy this arrow to a new name, the arrow still points to the same list object. This is what happens when we create an **alias**. To avoid aliasing, you need to copy the list. We have shown above on how you can create a copy of a list. + + +## Traversing a List + +One of the common operations when we deal with a list data type is to **traverse** the list. This means that we would like to visit every element of the list and process it in some way. The previous section has shown one simple way to traverse a list. The syntax is shown below. + +```python +for element in iterable: + # block A for code to be repeated + # do something with element +``` + +This syntax is similar when we traverse every character of a string. Recall the following code. + +```python +name = "John Wick" +for char in name: + print(char) +``` + +List is also an *iterable* just like string data type. Because of this, we can actually traverse the list in a similar way. + +```python +my_steps: list[int] = [40, 50, 43, 55, 67, 56, 60] +output = [] +for step in my_steps: + cadence = compute_cadence_for_30sec(step) + output.append(cadence) +``` + +In the above code `my_steps` is our list which is an iterable. The variable `step` takes in the element of `my_steps` at *every iteration*. We can see the value of `step` by adding the `print()` statement after the for statement as shown below. Click the "next" button to step through the iteration in the for loop. + + + +We can also use the `enumerate()` function to count the element and get the element at the same time. For example, we can use the code below to display the day and the steps. + +```python +my_steps: list[int] = [40, 50, 43, 55, 67, 56, 60] +for index, step in enumerate(my_steps): + print('Day ', (index + 1), ', steps: ', step) +``` + +Recall that enumerate returns an iterable where each element is a tuple. The first element of the tuple is the index and the second element of that tuple is the list element that we are enumerating. To capture this tuple, we set the variable between `for` and `in` as the following: `index, step`. Since the enumeration starts from 0 and we want to count the day from 1, we use `index + 1` inside our `print()` statement. Use the below Python Tutor to step through the code and observe the output. + + + +Lastly, we can also traverse the list by creating the index and traversing the range of the indices. + +```python +my_steps: list[int] = [40, 50, 43, 55, 67, 56, 60] +for index in range(len(my_steps)): + print('Day ', (index + 1), ', steps: ', my_step[index]) +``` + +Notice that the argument for `range()` function is an integer and not a list. In this case, we specify `len(my_steps)` as the input to `range()` function. The length of the list is 7 and so it calls `range(7)` which results in an iterable from 0 to 6 (exclusive of the ending) for the variable `index`. To retrieve the element of the list, we use the get item operator (square bracket), i.e. `my_step[index]`. + +## Identifying When to Use a Tuple or a List + +List looks a lot like tuple that we learned in the previous lesson. So when do we use tuple and when do we use list? One main difference between tuple and list that we have discussed is that tuple is immutable while list is mutable. This means that we use tuple when there is no need to change the element of the tuple. On the other hand, if our solution requires change of the elements of the collection data, it would be preferable to use list instead. + +Another common practice when programmers use tuple is when we just want to group a collection of data which maybe related but rather different. In the example above, for example, we use tuple for contact info. + +```python +contact_info: tuple = ("John Wick", 81234567, "john@wick.com") +``` + +Notice that though these three data are related since they are all information of the same person, they are different kinds of data. The first one is a string since it is the name of the person. The second one is a number for the contact. The last one is the email address. Tuple is a convenient way to group them together. There are better ways which we will explore in subsequent lesson such as using dictionary. However, list is usually used to group items that is very look a like. In this lesson, we used it to group the steps of various days. + +```python +my_steps: list[int] = [40, 50, 43, 55, 67, 56, 60] +``` + +Notice that all these data are integers and they are all `steps`. The only thing that differentiate them is which day the step belongs to. This comes to the other characteristics of list data. List data naturally suits those that have a sequence. In this case, the first data is the step on the first day, the second data is the step on the second day and so on. This suits list since each element is placed with a fixed index. The indices in a list data type is useful when the data has a certain order or sequence. Data at index of higher value is at a later sequence as compared to data at a lower value index. + +Because of this, usually list is used when the data is of the same type. Though Python allows a mixed data type in a list, it is common to apply list for those collections that have the same data type. On the other hand, it is common to use tuple for those collections that have various data type in their elements. + +## Finding a Maximum in a List Manually + +Now, we have introduced list data structure and some of its basic operations, it is time to put it into practice. List is a very common data structure that is frequently used in computer codes. Some programmers used list data type for anything that requires a collection of items. In this notes, we will differentiate the various collection data types and try to discuss which is the best to use. + +In this last section, we would like to show how we can use list for our application. Let's say, our app user wants to find which day he or she actually clock in the maximum number of steps. This requires us to look into all the elements of the list and find the maximum. This is a common problem and solving this problem help us to see some common patterns when working with list. We will do this in two ways. The first one we will just show you how to do this using some Python's built-in function. The second one, however, we will work this problem using just Python keywords such as for-loop and if-else statement. + +### (P)roblem Statement + +Let's start solving this problem. First, we would like to define the problem following the PCDIT framework. In the P step, we need to find out what is the input and output and write the problem statement. + +``` +Input: a list of steps in a week +Output: which day has the largest number of steps +Problem Statement: + given a list of steps in a week, + the function should returns which day in that week + has the largest number of steps +``` + +One important thing in the Problem Statement is to identify the input and output data types. Let's include this. + +``` +Input: a list of steps in a week (list of integers) +Output: which day has the largest number of steps (string) +Problem Statement: + given a list of steps in a week, + the function should returns which day in that week + has the largest number of steps +``` + +We want to be able to output the name of the day such as "Sunday" or "Monday". However, since we store the number of steps in a list where each day is associated with the position in that list, we will break this problem into two sub problems. + +``` +Sub-Problem 1: +Input: a list of steps in a week (list of integers) +Output: index of day with the largest number of steps (int) +Problem Statement: + given a list of steps in a week, + the function should return which day in that week + has the largest number of steps +``` + +``` +Sub-Problem 2: +Input: index of day in a week (integer) +Output: name of the day in a week (string) +Problem Statement: + given an index of day in a week, + the function should return the name + of the day in the week. +``` + +### Concrete (C)ases + +Now, we can work on some concrete cases for these two sub-problems. For sub-problem 1, we can have the following input as an example of a concrete case. + +```python +my_steps = [40, 50, 43, 55, 67, 56, 60] +``` + +We have to make an assumption on the index here. Let's say the week starts with Sunday on index 0. This means that, we have the following. + +| name of day | Sunday | Monday | Tuesday | Wednesday | Thursday | Friday | Saturday | +|-------------|--------|--------|---------|-----------|----------|--------|----------| +| index | 0 | 1 | 2 | 3 | 4 | 5 | 6 | +| element | 40 | 50 | 43 | 55 | 67 | 56 | 60 | + +The output of sub-problem 1 is the index of the day where the steps is the largest. To do this, we have two steps: +1. Find the largest element +2. Find the position of the largest element + +In the above example, the largest element is `67`. Once we found the largest element, we can return the position which is index `4`. + +Sub-problem 2 takes in the output of the first sub-problem which in this case index `4`. The output of the second sub-problem is to return the name of the day. Using the table above, we can see that the output is `Thursday`. + +### Using Built-in Function + +Python provides various built-in functions to work with its built-in data structures. In this part, we will digress a little bit and show how we can solve the above problem using Python's built-in function. In the next part, we will continue the PCDIT steps to show how it can be done manually using some basic control structures. + +The Concrete (C)ases above shows that we can solve this with two steps. First, we can find the maximum element and then find the position of that maximum element. Let's write down the solution step by step. We will first define the function and just check the input argument first. + +```python +def find_pos_of_max(list_steps: List[int]) -> int: + print(list_steps) + +my_steps: list[int] = [40, 50, 43, 55, 67, 56, 60] +day_max_step: int = find_pos_of_max(my_steps) +``` + +The output is shown below. + +``` +[40, 50, 43, 55, 67, 56, 60] +``` + +We can now find the maximum element using the `max()` function from Python and modify our function as follows. + +```python +def find_pos_of_max(list_steps: list[int]) -> int: + max_step: int = max(list_steps) + print(max_step) + +my_steps: list[int] = [40, 50, 43, 55, 67, 56, 60] +day_max_step: int = find_pos_of_max(my_steps) +``` + +The output now gives us the following which is the maximum step in that list. + +``` +67 +``` + +Now, we can find the position of this element in the list using `list.index(element)` function. + +```python +def find_pos_of_max(list_steps: list[int]) -> int: + max_step: int = max(list_steps) + pos: int = list_steps.index(max_step) + return pos + +my_steps: list[int] = [40, 50, 43, 55, 67, 56, 60] +day_max_step: int = find_pos_of_max(my_steps) +print(day_max_step) +``` + +The output is given as follows. + +``` +4 +``` + +Notice that we use the `return` statement such that this function outputs the position of the largest step. The output of this function is stored in `day_max_step` in the second last line which is then printed in the last line of the code. + +The Python code, without type annotation is given below and you can click step to check its execution step by step. + + + +### Concrete (C)ases for Manual Solution + +We have shown how to solve sub-problem 1 using the built-in function. But now, we will work out the solution if we were only to use the basic control structure. We will start by finding the largest element. + +How do we find the largest element in the list? We have to look into every element and compare them. But in our case, we have to act and think like a computer where we can only traverse the element one at a time. Let's start with out input list again. + +```python +my_steps = [40, 50, 43, 55, 67, 56, 60] +``` + +The index and the name of the day is given in the table below. + +| name of day | Sunday | Monday | Tuesday | Wednesday | Thursday | Friday | Saturday | +|-------------|--------|--------|---------|-----------|----------|--------|----------| +| index | 0 | 1 | 2 | 3 | 4 | 5 | 6 | +| element | 40 | 50 | 43 | 55 | 67 | 56 | 60 | + +Since we can only traverse element of a list one at a time, we will indicate which element we are at using an arrow. So how do we traverse the list and find the maximum? First, we will take the first element as the *largest* element so far. + +```python +largest_so_far = 40 +``` + +then, we will traverse the element starting from the second element all the way to the end. + +| name of day | Sunday | Monday | Tuesday | Wednesday | Thursday | Friday | Saturday | +|-------------|--------|--------|---------|-----------|----------|--------|----------| +| index | 0 | 1 | 2 | 3 | 4 | 5 | 6 | +| element | 40 | 50 | 43 | 55 | 67 | 56 | 60 | +| current | | ^ | | | | | | + +The current arrow points to element `50` for the first iteration. We can then compare the current element with the largest so far. In this case, since `50 > 40`, we will replace the largest so far to `50`. + +```python +largest_so_far = 50 +``` + +We can then move on to the next element. + +| name of day | Sunday | Monday | Tuesday | Wednesday | Thursday | Friday | Saturday | +|-------------|--------|--------|---------|-----------|----------|--------|----------| +| index | 0 | 1 | 2 | 3 | 4 | 5 | 6 | +| element | 40 | 50 | 43 | 55 | 67 | 56 | 60 | +| current | | | ^ | | | | | + +At this iteration, we compare if `43` is larger than `50`. Since it is not, we keep the largest so far to be `50` and move to the next element. + +| name of day | Sunday | Monday | Tuesday | Wednesday | Thursday | Friday | Saturday | +|-------------|--------|--------|---------|-----------|----------|--------|----------| +| index | 0 | 1 | 2 | 3 | 4 | 5 | 6 | +| element | 40 | 50 | 43 | 55 | 67 | 56 | 60 | +| current | | | | ^ | | | | + +At this iteration, `55 > 50` and so we found a new larger number. So we have to update our largest so far. + +```python +largest_so_far = 55 +``` + + +| name of day | Sunday | Monday | Tuesday | Wednesday | Thursday | Friday | Saturday | +|-------------|--------|--------|---------|-----------|----------|--------|----------| +| index | 0 | 1 | 2 | 3 | 4 | 5 | 6 | +| element | 40 | 50 | 43 | 55 | 67 | 56 | 60 | +| current | | | | | ^ | | | + +At this iteration, we compare again and found that `67 > 55`. So we have to update our largest so far again. + +```python +largest_so_far = 67 +``` + +| name of day | Sunday | Monday | Tuesday | Wednesday | Thursday | Friday | Saturday | +|-------------|--------|--------|---------|-----------|----------|--------|----------| +| index | 0 | 1 | 2 | 3 | 4 | 5 | 6 | +| element | 40 | 50 | 43 | 55 | 67 | 56 | 60 | +| current | | | | | | ^ | | + +At this iteration, we compare `56` and `67`. However, since 56 is not greater than the larges so far, we can keep 67 as the largest number so far and move to the last element. + +| name of day | Sunday | Monday | Tuesday | Wednesday | Thursday | Friday | Saturday | +|-------------|--------|--------|---------|-----------|----------|--------|----------| +| index | 0 | 1 | 2 | 3 | 4 | 5 | 6 | +| element | 40 | 50 | 43 | 55 | 67 | 56 | 60 | +| current | | | | | | | ^ | + +At this last iteration, we compare `60` with `67` but `67` is still the largest so far. Since we have no more element to compare in the list, we finish iteration. The final result is found in the `largest_so_far` bucket which currently contains `67`. We can now try to write down the steps above. + +### (D)esign of Algorithm + +The purpose of this (D)esign of Algorithm step is to generalize the steps we have done in Concrete (C)ases. Let's write down our steps here. + +``` +1. Set the first element in the list as the largest_so_far +2. Iterate from the second element to the end, and do the following: + 2.1 Get the value of the current element + 2.2 If the current element > largest_so_far, do the following: + 2.2.1 Set the current element as the largest so far +``` + +### (I)mplementation and (T)esting + +Now, we can try to implement the above steps in Python. Let's write down the function definition and the code to test it first. + +```python +def find_pos_of_max(list_steps: list[int]) -> int: + print(list_steps) + +my_steps: list[int] = [40, 50, 43, 55, 67, 56, 60] +day_max_step: int = find_pos_of_max(my_steps) +``` + +The output of the above code just printed the input argument. + +``` +[40, 50, 43, 55, 67, 56, 60] +``` + +Let's, do step 1 and print the value of the largest so far. + +```python +def find_pos_of_max(list_steps: list[int]) -> int: + largest_so_far: int = list_steps[0] + print(largest_so_far) + return largest_so_far + +my_steps: list[int] = [40, 50, 43, 55, 67, 56, 60] +day_max_step: int = find_pos_of_max(my_steps) +``` + +The output of the above code is shown below. +``` +40 +``` + +Currently, the largest number is the first day, which is `40`. Now, we can do the second step to iterate from the second element to the last. To iterate from the second element, we can slice the list as shown in this chapter. + +```python +list_steps[1:] +``` + +We will use that together with `for` statement to traverse the list element from the second to the end. We will print each element at every iteration for testing. Let's modify the code to do that. + +```python +def find_pos_of_max(list_steps: list[int]) -> int: + largest_so_far: int = list_steps[0] + for current_element in list_steps[1:]: + print(current_element) + return largest_so_far + +my_steps: list[int] = [40, 50, 43, 55, 67, 56, 60] +day_max_step: int = find_pos_of_max(my_steps) +``` + +The output is shown below. + +``` +50 +43 +55 +67 +56 +60 +``` + +We can see that the first output is actually the step number in the second element, i.e. `50`. So now, we have managed to iterate the element of the list from the second to the end, i.e. `60`. In this process, we have actually also completed step 2.1 since we already got it in the variable `current_element` as we iterate the list. + +We can now do step 2.2. To compare two numbers, we will use the `if` statement. + +```python +def find_pos_of_max(list_steps: list[int]) -> int: + largest_so_far: int = list_steps[0] + for current_element in list_steps[1:]: + if current_element > largest_so_far: + largest_so_far = current_element + print(current_element, largest_so_far) + return largest_so_far + +my_steps: list[int] = [40, 50, 43, 55, 67, 56, 60] +day_max_step: int = find_pos_of_max(my_steps) +``` + +We added the line `print(current_element, largest_so_far)` so that we can see what is the current element at each iteration and see what is the value of the largest number. The output is given below. + +``` +50 50 +43 50 +55 55 +67 67 +56 67 +60 67 +``` + +In the output above, the first column is the `current_element` and the second column is the `largest_so_far`. We can see that the variable `largest` so far is updated when the `current_element` is 50, 55, and finally 67. After which, the largest element stays at 67. So it seems we have completed the steps to find the largest number in the list. Now, we can remove the print statement and return this largest number. + +```python +def find_pos_of_max(list_steps: list[int]) -> int: + largest_so_far: int = list_steps[0] + for current_element in list_steps[1:]: + if current_element > largest_so_far: + largest_so_far = current_element + return largest_so_far + +my_steps: list[int] = [40, 50, 43, 55, 67, 56, 60] +day_max_step: int = find_pos_of_max(my_steps) +print(day_max_step) +``` + +In the above code, we have added `print(day_max_step)` at the last line to print the output of the function. The output is shown below. + +``` +67 +``` + +You may notice, however, that we output the wrong result. The function should not output the largest number but the position of the largest number. We have not finished. In order to find the position of the largest number of steps, we need to keep track, which day we found the largest step. Let's modify our steps. + + +``` +1. Set the first element in the list as the largest_so_far +2. Set the day_largest_step to the position of the first element +3. Iterate from the second element to the end, and do the following: + 3.1 Get the value of the current element + 3.2 Get the position of the current element + 3.2 If the current element > largest_so_far, do the following: + 3.2.1 Set the current element as the largest so far + 3.2.2 Set the current position as the day_largest_step +``` + +In the above, step, we have added step 2, 3.2 and 3.2.2. In step 2, we set the position of the first day as the position of the largest step. At each iteration, we keep track what is the day position in step 3.2. The only change, we need, now, is that whenever we update the largest step, we also update its position. This is done in step 3.2.2. + +Let's implement those three steps. Let's start with implementing step 2. + +```python +def find_pos_of_max(list_steps: list[int]) -> int: + largest_so_far: int = list_steps[0] + pos_largest: int = 0 # step 2 + for current_element in list_steps[1:]: + if current_element > largest_so_far: + largest_so_far = current_element + return pos_largest # now the output returns the position instead + +my_steps: list[int] = [40, 50, 43, 55, 67, 56, 60] +day_max_step: int = find_pos_of_max(my_steps) +print(day_max_step) +``` + +In the above code, we have added `pos_largest` and modify the return statement to return this variable instead. If we run this code, we will get 0 as the output. The reason is that we have not updated `pos_largest` whenever we updated the `largest_so_far`. To do this, we need to implement step 3.2 and 3.2.2. + +How do we do step 3.2. In the previous section, we discussed the function `enumerate()` which outputs a tuple of index and element of the list. We can use that to get the position of the current element. + + + +```python +def find_pos_of_max(list_steps: list[int]) -> int: + largest_so_far: int = list_steps[0] + pos_largest: int = 0 # step 2 + for pos, current_element in enumerate(list_steps[1:]): + if current_element > largest_so_far: + largest_so_far = current_element + print(pos, current_element, largest_so_far) + return pos_largest # now the output returns the position instead + +my_steps: list[int] = [40, 50, 43, 55, 67, 56, 60] +day_max_step: int = find_pos_of_max(my_steps) +print(day_max_step) +``` + +In the above code, we have added back the print statement to print the current position, the current element, and the largest number so far. The output is shown below. + +``` +0 50 50 +1 43 50 +2 55 55 +3 67 67 +4 56 67 +5 60 67 +0 +``` + +The first column shows the current position, the second column shows the current element and the last one shows the largest so far. The last line is the output of the function which currently still outputs 0. Now, we have found a way to get the current position, we can implement step 3.2.2 to update this position. + +```python +def find_pos_of_max(list_steps: list[int]) -> int: + largest_so_far: int = list_steps[0] + pos_largest: int = 0 # step 2 + for pos, current_element in enumerate(list_steps[1:]): + if current_element > largest_so_far: + largest_so_far = current_element + pos_largest = pos + print(pos, current_element, largest_so_far) + return pos_largest # now the output returns the position instead + +my_steps: list[int] = [40, 50, 43, 55, 67, 56, 60] +day_max_step: int = find_pos_of_max(my_steps) +print(day_max_step) +``` + +We have added the line `pos_largest = pos` inside the true block when the codition of the comparison is true. This means that we will update the `pos_largest` whenever we found there is a current element that is larger than the largest so far. The output of the function is shown below. + +``` +0 50 50 +1 43 50 +2 55 55 +3 67 67 +4 56 67 +5 60 67 +3 +``` + +Notice that we got `3` as the day of the largest step. This is incorrect since our Concrete (C)ases shows we should get `4` instead. The reason of this mistake is that when we enumerate the list, our enumerate function starts counting from 0 whereas our list is actually starting from day 1 which is the second day. Therefore, to get the correct answer, we need to add the current position by 1. Let's fix the code and see the output once again. + + +```python +def find_pos_of_max(list_steps: list[int]) -> int: + largest_so_far: int = list_steps[0] + pos_largest: int = 0 # step 2 + for pos, current_element in enumerate(list_steps[1:]): + if current_element > largest_so_far: + largest_so_far = current_element + pos_largest = pos + 1 + print(pos, current_element, largest_so_far) + return pos_largest # now the output returns the position instead + +my_steps: list[int] = [40, 50, 43, 55, 67, 56, 60] +day_max_step: int = find_pos_of_max(my_steps) +print(day_max_step) +``` + +In the above code, we have modified the line to `pos_largest = pos + 1`. The output now is shown as follows. + +``` +0 50 50 +1 43 50 +2 55 55 +3 67 67 +4 56 67 +5 60 67 +4 +``` + +This time the output of `day_max_step` is `4`. + +In the code below, we remove the type annotation to make it simpler and you can trace the execution using Python Tutor. + + + +The final function should not have print statement inside it and it is given here. + +```python +def find_pos_of_max(list_steps: list[int]) -> int: + largest_so_far: int = list_steps[0] + pos_largest: int = 0 # step 2 + for pos, current_element in enumerate(list_steps[1:]): + if current_element > largest_so_far: + largest_so_far = current_element + pos_largest = pos + 1 + return pos_largest # now the output returns the position instead + +my_steps: list[int] = [40, 50, 43, 55, 67, 56, 60] +day_max_step: int = find_pos_of_max(my_steps) +print(day_max_step) +``` + +This is the solution only for the first sub-problem. The second sub-problem to get the name of the day given the index of the day in a week can be solved using `if-else` statement. This is shown below. + +```python +def get_name_of_day(index: int) -> str: + if index == 0: + return "Sunday" + elif index == 1: + return "Monday" + elif index == 2: + return "Tuesday" + elif index == 3: + return "Wednesday" + elif index == 4: + return "Thursday" + elif index == 5: + return "Friday" + else: + return "Saturday" +``` + +The above function assumes the input is in the range from 0 to 6 only. However, there is a better way of writing this piece of code when we learn another data structure called Dictionary. Until then, we can simply use the if-else statement as above. + +### Function Composition + +We have created two functions `find_pos_of_max()` and `get_name_of_day()`. These two functions solve the two sub problems separately. We can then make use of these two functions to solve our original problem. The image below show the flowchart. + +INSERT IMAGE FLOWCHART + +We have learnt that our function can call another function and this is what is called as function composition. This means that we can compose our function using other functions as part of its body. Let's define our final function. + +```python +def find_day_of_max_steps(list_steps: list[int]) -> str: + day_index: int = find_pos_of_max(list_steps) + day_name: str = get_name_of_day(day_index) + return day_name + +my_steps: list[int] = [40, 50, 43, 55, 67, 56, 60] +day_max_step: str = find_day_of_max_steps(my_steps) +print(day_max_step) +``` + +In the above code, we first call `find_pos_of_max()` to get the index of the day that has the maximum step in the week. We then put this index as an input to our second function `get_name_of_day()` which outputs the string. The output of the above function is shown below. + +``` +Thursday +``` + +### Finding Position of Maximum Item in Any List + +One thing to note is that we purposely name our functions generic enough such as `find_pos_of_max()` instead of `find_pos_of_max_step()`. The reason is that this function can be used to find the position of any list and not only steps. It is general enough and can be used for any other purpose whenever we want to find the index of the maximum item in the list. To make express this, we should rename some of the variables to make it general. + +```python +from typing import List + +def find_pos_of_max(list_items: list[int]) -> int: + largest_so_far: int = list_steps[0] + pos_largest: int = 0 # step 2 + for pos, current_element in enumerate(list_items[1:]): + if current_element > largest_so_far: + largest_so_far = current_element + pos_largest = pos + 1 + return pos_largest # now the output returns the position instead +``` + +In the above code, we replaced the name `list_steps` into `list_items`. + +### Traversing From the First Element + +One other improvement we can do is to generalize the code such that we traverse the elements in the list starting from the first element instead from the second element. In the above solution, we first put the first element as the largest element before we start the iteration. There is a way if we want to traverse starting from the first element. We just need to make sure that when we compare the first element, it will always replace the first initial value. When finding the largest element, we can do so by initializing this to the smallest number. In our case, it is enough to initialize the largest element with a negative number since number of steps are non-negative. We can then re-write our solution as follows. + +```python +def find_pos_of_max(list_items: list[int]) -> int: + largest_so_far: int = -1 # use negative number to initialize + pos_largest: int = -1 # we need to initialize the position as well + for pos, current_element in enumerate(list_items): # now we are iterating all elements + if current_element > largest_so_far: + largest_so_far = current_element + pos_largest = pos # now, we do not need to add by 1 anymore + return pos_largest +``` + +We made four modifications in the above code. The first is that we set the `largest_so_far` to be negative. This ensures that when we visit the first element, that first element will be larger and set as the `largest_so_far`. Second, we need to initialize the position of the largest element as well. We chose to initialize it to -1 for similar reason assuming the index of the day we handle is from 0 to 6 for Sunday to Saturday. Third, our `enumerate(list_items)` now contains no slicing. We visit all elements in the list starting from the first element. Lastly, because, now, we iterate from the first element, the `enumerate()` functions outputs its `pos` value for the fist day in the week and there is no need for us to do `pos + 1` as in the case when we start from the second day. Since we start enumerating from the first day, the `pos` value is the same index as the day in the week. Thus, we can just set `pos_largest = pos`. + + +## Summary + +In this section, we introduce the most commonly used collection data type in Python, which is list. We showed the various basic operation for list and finally we apply some of these to our problem of finding the day of the maximum step. Given a list, we want to find which day has the largest step. We showed two ways to do this. The first one uses Python's built-in function and the second one we went through the whole steps of PCDIT to derive the final solution. We purposely do the implementation and testing in steps so that we learn how to test the code in small bite size. Moreover, we solve the problem also in small steps. We divide the problem into two sub-problems and solve it separately. The important part in this lesson is to know how create list and how work with list data type. When working with list data type, one of the important thing is to traverse the list and do some computation with the element of the list. This shown in our example of finding the day of the maximum step. + +We also applied some concepts we learnt previously such as function composition. We can assemble our function by calling other functions. Moreover, we showed how we can improve our solutions by generalizing our function. \ No newline at end of file diff --git a/_Notes/Logistic_Regression.md b/_Notes/Logistic_Regression.md deleted file mode 100644 index 2e10004..0000000 --- a/_Notes/Logistic_Regression.md +++ /dev/null @@ -1,439 +0,0 @@ ---- -title: Logistic Regression for Classification -permalink: /notes/logistic_regression -key: notes-logistic-regression -layout: article -nav_key: Notes -sidebar: - nav: Notes -license: false -aside: - toc: true -show_edit_on_github: false -show_date: false ---- - -## Introduction - -The problem of classification deals with *categorical* data. In this problem, we wish to identify a set of data whether they belong to a particular class of category. For example, a given text message from an email, we would like to classify if it is a spam or not a spam. Another example would be given some measurement of cancer cells we wish to classify if it is benign or malignant. In this section we will learn logistic regression to solve this classification problem. - -## Hypothesis Function - -Let's take an example of breast cancer classification problem. Let's say depending on the cell size, an expert can identify if the cell is benign or malignant. We can plot something like the following figure. - -![](/assets/images/week10/cancer_cell_plot.png) - -In the y-axis, value of 1 means it is a malignant cell while value of 0 means it is benign. The x-axis can be considered as a normalized size of the cell with mean 0 and standard deviation of 1 (recall z-normalization). - -If we can model this plot as a function $p(x)$, we can set the following criteria to classify the cells. For example, we will predict it is malignant if $p(x) \geq 0.5$, otherwise, it is benign. This means we need a function where we can model the data in a step wise manners and fulfills the following: - -$$0 \leq p(x) \leq 1$$ - -where $p(x)$ is the probability that a cell with feature $x$ is a malignant cell. - -One of the function that we can use that have this step-wize shape and the above properties is a logistic function. A logistic function can be written as. - -$$y = \frac{1}{1 + e^{-z}}$$ - -The plot of a logistic function looks like the following. - - -```python -import numpy as np -import matplotlib.pyplot as plt - -z = np.array(range(-10,11)) -y = 1/(1+np.exp(-z)) -plt.plot(z,y) -``` - - - - - [] - - - - -![png](/assets/images/week10/Logistic_Regression_4_1.jpeg) - - -We can write our hypothesis as follows. - -$$p(x) = \frac{1}{1 + e^{-z(x)}}$$ - -where $z$ is a function of $x$. What should be this $z$ function. We can then use our linear model of a straight line and transform it into a logistic function if we use the following transformation. - -$$z(x) = \beta_0 x_0 + \beta_1 x_1$$ - -when $x_0 = 1$, the above equation is simply the straight line equation of linear regression. - -$$\beta_0 + \beta_1 x_1$$ - -This is the case when we only have one feature $x_1$. If we have more than one feature, we should write as follows. - -$$z(x) = \beta_0 x_0 + \beta_1 x_1 + \ldots + \beta_n x_n$$ - -Note that in this notes we tend to omit the *hat* symbol to indicate it is the estimated parameters as in the previous notes. We will just indicate the estimated parameters as $\beta$ instead of $\hat{\beta}$. - -The above relationship shows that we can map the value of linear regression into a new function with a value from 0 to 1. This new function $p(x)$ can be considered as *an estimated probability* that $y = 1$ on input $x$. For example, if $p(x) = 0.7$ this means that 70% chance it is malignant. We can then use the following boundary conditions: -- y = 1 (malignant) if $p(x) \geq 0.5$ -- y = 0 (benign) if $p(x) < 0.5$ - -The above conditions also means that we can classify $y=1$ when $\beta^T x \geq 0$ and $y = 0$ when $\beta^T x < 0$. We can draw these boundary conditions. - -![](/assets/images/week10/decision_boundary.png) - -In the figure above, we indicated the predicted label $y$ with the orange colour. We see that when $p(x)\geq 0.5$, the data is marked as $y=1$ (orange). On the other hand, when $p(x) \leq 0.5$, the data is marked as $y=0$ (orange). The thick black line shows the decision boundary for this particular example. - -How do we get this boundary decision. Once we found the estimated values for $\beta$, we can find the value of $x$ which gives $\beta^Tx = 0$. You will work on computing the parameters $\beta$ in the problem set. For now, let's assume that you manage to find the value of $\beta_0 = -0.56$ and $\beta_1 = 1.94$. The equation $\beta^T x = 0 $ can be written as follows. - -$$\beta_0 + \beta_1 x = 0$$ - -We can then substitute the values for $\beta$ into the equation. - -$$-0.56 + 1.94 x = 0$$ -$$x = 0.29 \approx 0.3$$ - -From the figure above, this fits where the thick line is, which is at around 0.3. - -## Cost Function - -Similar to linear regression, our purpose here is to find the parameters $\beta$. To do so, we will have to minimize some cost function using optimization algorithm. - -For logistic regression, we will choose the following cost function. - -$$J(\beta) = \frac{1}{m} \Sigma_{i=1}^m \left\{ \begin{matrix} --\log(p(x)) & \text{ if } y = 1\\ --\log(1 - p(x)) & \text{ if } y = 0 -\end{matrix}\right.$$ - -We can try to understand the term inside the bracket intuitively. Let's see the case when $y=1$. In this case, the cost term is given by: - -$$-\log(p(x))$$ - -The cost is 0 if $y = 1$ and $p(x) = 1$ because $-\log(z)$ is 0 when $z=1$. Moreover, as $p(x) \rightarrow 0$, the cost will reach $\infty$. [See plot by wolfram alpha](https://www.wolframalpha.com/input/?i=-log%28x%29+from+0+to+1). - -On the other hand, when $ y = 0$, the cost term is given by: - -$$-\log(1-p(x))$$ - -In this case, the cost is 0 when $p(x) = 0$ but it reaches $\infty$ when $p(x) \rightarrow 1$. [See plot by wolfram alpha](https://www.wolframalpha.com/input/?i=-log%281-x%29+from+0+to+1). - -We can write the overall cost function for all the data points from $i=1$ to $m$ as follows. - -$$J(\beta) = -\frac{1}{m}\left[\Sigma_{i=1}^m y^i \log(p(x^i)) + (1 - y^i) \log(1 - p(x^i))\right]$$ - -Notice that when $y^i = 1$, the function reduces to - -$$J(\beta) = -\frac{1}{m}\left[\Sigma_{i=1}^m \log(p(x^i)) \right]$$ - -and when $y^i = 0$, the function reduces to - -$$J(\beta) = -\frac{1}{m}\left[\Sigma_{i=1}^m \log(1 - p(x^i))\right]$$ - -## Gradient Descent - -We can find the parameters $\beta$ again by using the gradient descent algorithm to perform: - -$$\begin{matrix} -min & J(\beta)\\ -\beta & \end{matrix}$$ - -The update functions for the parameters are given by - -$$\beta_j = \beta_j - \alpha \frac{\partial}{\partial \beta_j} J(\beta)$$ - -The derivative of the cost function is given by - -$$\frac{\partial}{\partial \beta_j}J(\beta) = \frac{1}{m}\Sigma_{i=1}^m \left(p(x)-y^i \right)x_j^i$$ - -See the appendix for the derivation. We can substitute this in to get the following update function. - -$$\beta_j = \beta_j - \alpha \frac{1}{m}\Sigma_{i=1}^m \left(p(x)-y^i \right)x_j^i$$ - - -## Matrix Notation - -The above equations can be written in matrix notation so that we can perform a vectorized computation. - -### Hypothesis Function - -Recall that our hypothesis can be written as: - -$$p(x) = \frac{1}{1 + e^{-z(x)}}$$ - -where - -$$z(x) = \beta_0 x_0 + \beta_1 x_1 + \ldots + \beta_n x_n$$ - -We can write this equation as vector multiplication as follows. - -$$z = \mathbf{b}^T \mathbf{x}$$ - -and - -$$p(x) = \frac{1}{1 + e^{-\mathbf{b}^T \mathbf{x}}}$$ - -where - -$$\mathbf{b} = \begin{bmatrix} -\hat{\beta}_0\\ -\hat{\beta}_1 \\ -\ldots\\ -\hat{\beta}_n -\end{bmatrix}$$ - -and -$$\mathbf{x} = \begin{bmatrix} -1 \\ -x_1 \\ -x_2 \\ -\ldots \\ -x_n \\ -\end{bmatrix}$$ - -Recall that this is for a single data with $n$ features. The result of this vector multiplication $z$ is a single number for that one single data with $n$ features. - - - -What about if we have more that one data. Let's say if we have $m$ rows of data, We have to rewrite the $\mathbf{x}$ vector as a matrix $\mathbf{X}$. - -$$\mathbf{X} = \begin{bmatrix} -1 & x_1^1 & x_2^1 & \ldots & x_n^1 \\ -1 & x_1^2 & x_2^2 & \ldots & x_n^2 \\ -\ldots & \ldots & \ldots & \ldots & \ldots \\ -1 &x_1^m & x_2^2 & \ldots & x_n^m -\end{bmatrix}$$ - -In the above notation, we put the features as the columns and the different row as different rows in the matrix. With $m$ rows of data, our $z$ values is now a vector $\mathbf{z}$. - -$$\mathbf{z} = \mathbf{b}^T \mathbf{X}^T$$ - -The above equation results in a **row** vector. If we prefer to keep it as a column vector, we can transpose it as follows. - -$$\mathbf{z} = (\mathbf{b}^T \mathbf{X}^T)^T$$ - -We can have the same column vector by putting the matrix $\mathbf{X}$ on the left hand side of the matrix multiplication. - -$$\mathbf{z} = \mathbf{X} \mathbf{b}$$ - -And now our hypothesis for $m$ rows of data can be written as - - -$$\mathbf{p}(x) = \frac{1}{1 + e^{-\mathbf{X}\mathbf{b}}}$$ - -### Cost Function - -Recall that the cost function for all the data points from $i=1$ to $m$ as follows. - -$$J(\beta) = -\frac{1}{m}\left[\Sigma_{i=1}^m y^i \log(p(x^i)) + (1 - y^i) \log(1 - p(x^i))\right]$$ - -Notice that when $y^i = 1$, the function reduces to - -$$J(\beta) = -\frac{1}{m}\left[\Sigma_{i=1}^m \log(p(x^i)) \right]$$ - -and when $y^i = 0$, the function reduces to - -$$J(\beta) = -\frac{1}{m}\left[\Sigma_{i=1}^m \log(1 - p(x^i))\right]$$ - -How can we vectorize this computation in Python? Numpy provides the function `np.where()` which we can use if we have more than one computation depending on certain conditions. - -For example, if we have an input `x = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]`, we can compute the value of y on whether $x$ is even or odd. Let's say, we will square the value if the value is even. On the other hand, we will leave the value as it is if it is odd. Below cell shows how the code can be written. - - -```python -# create a list from 0 to 9 -x = list(range(10)) - -# using np.where() -y = np.where(np.mod(x,2) == 0, np.power(x,2), x) -print(y) -``` - - [ 0 1 4 3 16 5 36 7 64 9] - - -We can, thus, use `np.where()` to calculate the cost function depending on whether $y^i$ is 1 or zero using the two equations above. The summation in the above equation can be computed using `np.sum()`. - -An example of using `np.sum()` can be seen in the below cell. - - -```python -# create a list from 0 to 9 -x = list(range(10)) - -# using np.sum() to sum up all the numbers in the vectors -y = np.sum(x) -print(y) -``` - - 45 - - -If you are dealing with a matrix, you can specify the axis of `np.sum()` whether you want to sum over the rows or the columns. By default is over the rows or `axis=0` in Numpy. - - -```python -x = [[1, 2, 3], [4, 5, 6]] -print(np.sum(x, axis=0)) - -``` - - [5 7 9] - - -In the above code we sum over the rows and so we have three values for each column. If we wish to sum over all the columns, we should do as the one below. - - -```python -x = [[1, 2, 3], [4, 5, 6]] -print(np.sum(x, axis=1)) -``` - - [ 6 15] - - -In the above output, we see that 6 is the sum of `[1, 2, 3]` and 15 is the sum of `[4, 5, 6]`. - -### Gradient Descent Update - -Recall that the update function in our gradient descent calculation was the following. - -$$\beta_j = \beta_j - \alpha \frac{1}{m}\Sigma_{i=1}^m \left(p(x)-y^i \right)x_j^i$$ - -We can write this a vectorized calculation particularly because we have the summation of some multiplication terms. This sounds like a good candidate for a matrix multiplication. Recall that our hypothesis for $m$ data points is a column vector. - -$$\mathbf{p}(x) = \frac{1}{1 + e^{-\mathbf{X}\mathbf{b}}}$$ - -Similarly, $y$ which is the actual target value from the training set can be written as a column vector of size $m\times 1$. Therefore, we can do the calculation element-wise for the following term. - -$$\mathbf{p} - \mathbf{y}$$ - -The result is a column vector too. - -The features $x_j^i$ can be arranged as a matrix as shown below. - -$$\mathbf{X} = \begin{bmatrix} -1 & x_1^1 & x_2^1 & \ldots & x_n^1 \\ -1 & x_1^2 & x_2^2 & \ldots & x_n^2 \\ -\ldots & \ldots & \ldots & \ldots & \ldots \\ -1 &x_1^m & x_2^2 & \ldots & x_n^m -\end{bmatrix}$$ - -We can do the multiplication and the summation as a matrix multiplication of the following equation. - -$$\mathbf{X}^T(\mathbf{p} - \mathbf{y})$$ - -Note that we transpose the matrix $\mathbf{X}$ so that it has the shape of $(1+n) \times m$. In this way, we can do matrix multiplication with $(\mathbf{p} - \mathbf{y})$ which has the shape of $m \times 1$. - -The rest of the computation is just a multiplication of some constants. So we can write our update function as follows. - -$$\mathbf{b} = \mathbf{b} - \alpha\frac{1}{m}\mathbf{X}^T(\mathbf{p} - \mathbf{y}) $$ - -## Multi-Class - -Since Logistic function's output range only from 0 to 1, does it mean that it can only predict binary classification, i.e. classification problem involving only two classes? The answer is no. We can extend the technique to apply to multi-class classification by using a technique called one-versus-all. - -The idea of one-versus-all technique is to reduce the multi-class classification problem to binary classification problem. Let's say we have three class and we would like to predict between cat, dog, and fish images. We can treat this problem as binary classification by predicting if an image is a cat or no cat. In this first instance, we treat both dog and fish images as a no-cat image. We then repeat the same procedures and try to predict if an image is a dog or a no-dog image. Similarly, we do the same with the fish and no-fish image. - -To facilitate this kind of prediction, instead of having **one** target column in the **training set** , we will be preparing **three** target columns, each column for each class. We need to prepare something like the following data set. - -| feature_1 | feature_2 | cat | dog | fish | -|-----------|-----------|-----|-----|------| -| x | x | 1 | 0 | 0 | -| x | x | 1 | 0 | 0 | -| x | x | 0 | 1 | 0 | -| x | x | 0 | 0 | 1 | -| x | x | 0 | 1 | 0 | - -We can then train the model **three times** and obtain the coefficients for **each class**. In this example, we would have **three sets** of beta coefficients, one for the cat versus no-cat, another one for dog versus no-dog, and the last one for fish versus no-fish. We can then use these coefficients to calculate the probability for each class and produce the probability - -Recall that our hypothesis function returns a probability between 0 to 1. - -$$\mathbf{p}(x) = \frac{1}{1 + e^{-\mathbf{Xb}}}$$ - -We can then construct three columns where each column contains the probability for the particular binary classification relevant to the column target. For example, we can have something like the following table. - -| feature_1 | feature_2 | cat | dog | fish | predicted class | -|-----------|-----------|-----|-----|------|-------| -| x | x | **0.8** | 0.2 | 0.3 | cat | -| x | x | **0.9** | 0.1 | 0.2 | cat | -| x | x | 0.5 | **0.9** | 0.4 | dog | -| x | x | 0.3 | 0.2 | **0.8** | fish | -| x | x | 0.1 | **0.7** | 0.5 | dog | - -In the above example, the first two rows have cat class as their highest probability. Therefore, we set "cat" as the predicted class in the last column. On the other hand, the third and the last row have "dog" as their highest probability and therefore, they are predicted as "dog". Similarly, with "fish" in the second last row. - -# Appendix - -## Derivation of Logistic Regression Derivative - - - -We want to find $\frac{\partial}{\partial \beta_j}J(\beta)$, where - -$$J(\beta) = -\frac{1}{m}\left[\Sigma_{i=1}^m y^i \log(p(x^i)) + (1 - y^i) \log(1 - p(x^i))\right]$$ - -To simplify our derivation, we will consider each case when $y=1$ and when $y=0$. When $y=1$, the cost function is given by - -$$J(\beta) = -\frac{1}{m}\left[\Sigma_{i=1}^m \log(p(x^i)) \right]$$ - -Derivating this with respect to $\beta$ is - -$$\frac{\partial}{\partial \beta_j}J(\beta) = -\frac{1}{m}\Sigma \frac{1}{p(x)}\frac{\partial}{\partial \beta}p(x)$$ - -Recall that the expression for the hypothesis is - -$$p(x) = \frac{1}{1 + e^{-\beta^T x}}$$ - -The derivative of this is given by - -$$\frac{\partial}{\partial \beta_j} p(x) = - \frac{1}{(1 + e^{-\beta^T x})^2} \times -x_j \times e^{-\beta^T x}$$ - -or - -$$\frac{\partial}{\partial \beta_j} p(x) = \frac{x_j e^{-\beta^T x}}{(1 + e^{-\beta^T x})^2} $$ - -We can then now substitute this back - -$$\frac{\partial}{\partial \beta_j}J(\beta) = -\frac{1}{m}\Sigma (1 + e^{-\beta^T x}) \frac{x_j e^{-\beta^T x}}{(1 + e^{-\beta^T x})^2}$$ - -$$\frac{\partial}{\partial \beta_j}J(\beta) = -\frac{1}{m}\Sigma \frac{x_j e^{-\beta^T x}}{(1 + e^{-\beta^T x})}$$ - -This can be written as - -$$\frac{\partial}{\partial \beta_j}J(\beta) = -\frac{1}{m}\Sigma p(x) x_j e^{-\beta^T x}$$ -This is for the case of $y = 1$. - -Now let's do the same for $y = 0$, the cost function is given by - -$$J(\beta) = -\frac{1}{m}\left[\Sigma_{i=1}^m \log(1 - p(x^i))\right]$$ - -Derivating this with respect to $\beta$ gives - -$$\frac{\partial}{\partial \beta_j}J(\beta) = \frac{1}{m}\Sigma \frac{1}{1 - p(x)}\frac{\partial}{\partial \beta}p(x)$$ - - -Substituting expression for the hypothesis function and its derivative gives us - -$$\frac{\partial}{\partial \beta_j}J(\beta) = \frac{1}{m}\Sigma \frac{1}{1 - \frac{1}{1 + e^{-\beta^T x}}} \frac{x_j e^{-\beta^T x}}{(1 + e^{-\beta^T x})^2} $$ - - -$$\frac{\partial}{\partial \beta_j}J(\beta) = \frac{1}{m}\Sigma \frac{1 + e^{-\beta^T x}}{e^{-\beta^T x}} \frac{x_j e^{-\beta^T x}}{(1 + e^{-\beta^T x})^2} $$ - -$$\frac{\partial}{\partial \beta_j}J(\beta) = \frac{1}{m}\Sigma \frac{x_j}{(1+e^{\beta^T x})} $$ - -$$\frac{\partial}{\partial \beta_j}J(\beta) = \frac{1}{m}\Sigma p(x) x_j$$ -This is for $y = 0$. - -Combining for both cases $y=0$ and $y=1$, we have - -$$\frac{\partial}{\partial \beta_j}J(\beta) = -\frac{1}{m}\Sigma_{i=1}^m y^i p(x) x_j e^{-\beta^T x} + (y^i - 1) p(x) x_j^i$$ - - -$$\frac{\partial}{\partial \beta_j}J(\beta) = -\frac{1}{m}\Sigma_{i=1}^m y^i p(x) x_j e^{-\beta^T x} + y^i p(x) x_j - p(x) x_j^i$$ - -$$\frac{\partial}{\partial \beta_j}J(\beta) = -\frac{1}{m}\Sigma_{i=1}^m \left(y^i p(x)(1 + e^{-\beta^T x}) - p(x) \right)x_j^i$$ -$$\frac{\partial}{\partial \beta_j}J(\beta) = -\frac{1}{m}\Sigma_{i=1}^m \left(y^i - p(x) \right)x_j^i$$ -$$\frac{\partial}{\partial \beta_j}J(\beta) = \frac{1}{m}\Sigma_{i=1}^m \left(p(x)-y^i \right)x_j^i$$ diff --git a/_Notes/Merge_Sort.md b/_Notes/Merge_Sort.md deleted file mode 100644 index 2132e21..0000000 --- a/_Notes/Merge_Sort.md +++ /dev/null @@ -1,224 +0,0 @@ ---- -title: Merge Sort -permalink: /notes/merge_sort -key: notes-merge-sort -layout: article -nav_key: Notes -sidebar: - nav: Notes -license: false -aside: - toc: true -show_edit_on_github: false -show_date: false ---- - -The previous example of summing an array gives a simple example of divide-and-conquer approach as an alternative to *iterative* solution. We also showed another problem which is easier to solve recursively as in the problem of Tower of Hanoi. Now, we will give another example which is intuitively recursive, merge sort algorithm. - -Merge sort follows the three steps *divide*, *conquer*, and *combine*. The idea of merge sort is split the input sequence into two parts (*divide*), and call merge sort recursively on each parts (*conquer*). After the two parts are sorted, it is then combined using *merge* step (*combine*). - -## Algorithm - -### (P)roblem Statement - -The problem statement is the same as any other sorting problem. Given an arbitrary sequence of numbers, say Integers, the problem is sequence this number in a certain order, say from smallest to largest. - -### Test (C)ase - -Let's give an example for a particular input sequence and see how merge sort solves the problem. - -``` -[16, 14, 10, 8, 7, 8, 3, 2, 4, 1] -``` - -We split the array into two parts recursively until each array is left only with one element. - -![](/assets/images/week3/mergesort_split.png) - -When we have the array with only one element, the array is trivially sorted. So now what we can do is to go up and merge the two array. This is shown in the figure below. - -![](/assets/images/week3/mergesort_merge.png) - -How do we merge the two arrays? We will give this example in the merge of the last step to get the final sorted array. - -The merge steps have three arrows as shown in the figure below, the *red*, *purple*, and *blue*. The red arrow points to the position of where to store the number in the sorted array. The purple arrow points to the number in the left array while the blue arrow points to the number in the right array. The merge step begins by comparing the number pointed by the purple arrow with the number pointed by the blue arrow. We then put the smaller number into the sorted array. - -![](/assets/images/week3/merge_steps01.png) - -We then move the arrow from which we move the number. In the example above 1 is smaller than 7, therefore, we put 1 into the position pointed by the red arrow and move the blue arrow to the next number. - -![](/assets/images/week3/merge_steps02.png) - -These steps continue as follows. - -![](/assets/images/week3/merge_steps03.png) - ---- - -![](/assets/images/week3/merge_steps04.png) - ---- - -![](/assets/images/week3/merge_steps05.png) - ---- -At this point, both left and right array have the same value, i.e. 8. We can choose arbitrarily that when the value is the same, we will take the value from the left array. - -![](/assets/images/week3/merge_steps06.png) - ---- - -![](/assets/images/week3/merge_steps07.png) - ---- - -At this point, we have finished putting the right array. So the subsequent steps simply filling up the sorted array from the left array. - -![](/assets/images/week3/merge_steps08.png) - ---- - -![](/assets/images/week3/merge_steps09.png) - ---- - -![](/assets/images/week3/merge_steps10.png) - -### (D)esign of Algorithm - -As shown in the previous section, we can divide Merge sort into two algorithm. The first algorithm is the main steps that contains the recursive calls. The second algorithm is the *merge* steps. We will discuss both algorithms below. - -In designing the main steps, we identify the recursive case and the base case. In our case, the base case is when the array contains only one element. In this case, the array is trivially sorted. Therefore, we do not need to do anything. On the other hand, when the number of element in the array is greater than one, we split the array into two, and call recursively the same steps, and combine them after they are sorted. We can write the algorithm in this way. - -``` -Merge Sort -Input: - - array = sequence of integers - - p = index of beginning of array - - r = index of end of array -Output: None, sort the array in place -Steps: -1. if r - p > 0, do: - 1.1 calculate q = (p + r) / 2 - 1.2 call MergeSort(array, p, q) - 1.3 call MergeSort(array, q+1, r) - 1.4 call Merge(array, p, q, r) -``` - -Note: -* We only consider the recursive case in step 1, i.e. when the length of the array we consider is more than one items. The base case is trivial since it is sorted if there is only one element. We know it is only one element if the end index and the beginning index is greater than 0. -* Then we calculate the middle point to split the array, i.e. $q = (p+r)/2$. -* We can then call the procedure recursively. Step 1.2 is to sort the left array which is from index $p$ to index $q$ while step 1.3 is to sort the right array which is from index $q+1$ to index $r$. -* Step 1.4 is the combine step by calling the *merge* procedure. - -We can now discuss the *merge* step algorithm. To do this step, we have three indices, we will call them *left* (purple), *right* (blue), and *dest* (red). The idea is to start from the beginning and compare the numbers pointed by the *left* and the *right* arrow. The smaller number will placed in position pointed by *dest*. - -``` -Merge -Input: -- array = sequence of integers -- p = beginning index of left array, which is also the beginning of the input sequence -- q = ending index of left array -- r = ending index of right array -Output: None, sort the array in place -Steps: -1. nleft = q - p +1 -2. nright = r - q -3. left_array = array[p...q] -4. right_array = array[(q+1)...r] -5. left = 0 -6. right = 0 -7. dest = p -8. As long as (left < nleft) AND (right < nright), do: - 8.1 if left_array[left] <= right_array[right], do: - 8.1.1 array[dest] = left_array[left] - 8.1.2 left = left + 1 - 8.2 otherwise, do: - 8.2.1 array[dest] = right_array[right] - 8.2.2 right = right + 1 - 8.3 dest = dest + 1 -9. As long as (left < nleft), do: - 9.1 array[dest] = left_array[left] - 9.2 left = left + 1 - 9.3 dest = dest + 1 -10. As long as (right < nright), do: - 9.1 array[dest] = right_array[right] - 9.2 right = right + 1 - 9.3 dest = dest + 1 -``` - -Note: -* Steps 1 and 2 are used to calculate the numbe of elements in the left and right arrays. -* Steps 3 and 4 are to copy the elements from the input array to the left and the right arrays. -* Step 5 and 6 are to initialize the position of the left and right array. We use index 0 here because we copied the numbers into two new arrays. The new arrays start with index 0. -* Step 7 is to initialize the *dest* arrow. It starts from *p* which is the starting of the sorted array. -* Step 8 is the merging steps by comparing the two arrays. -* Step 8.1 is the comparison to choose which element should be put into the sorted array. If the number in the left array is smaller than that number is placed into the sorted array (Step 8.1.1) and the arrow moves to the next number (Step 8.1.2). Otherwise, it will place the number from the right array (Step 8.2). -* Step 9 and 10 is to handle when the left array and the right array do not have the same length. In this case, the iteration in step 8 will terminate once the arrow reaches the end of the shorter array. In the figures above, we finish the right array before the left array. The last three figures above are simply the steps taken by Step 9 in putting the numbers 10, 14, and 16 into the sorted array. - -## Computation Time - -We will use the *recursive tree* method to analyze the computation time taken by Merge Sort. Looking into the pseudocode of Merge Sort, we can write the computation time as - -$$T_{mergesort}(n) = O(1) + 2T_{mergesort}(n/2)+T_{merge}(n)$$ - -Note: -* the constant time $O(1)$ comes from the comparison and the calculation of the index $q$. These takes constant time. -* there are two recursive call of merge sort for the left and the right right. This results in $2T_{mergesort}(n/2)$. -* there is a call to merge procedure which gives $T_{merge}(n)$. - -Therefore, in order to find the computation time for the merge sort, we need to look into the computation time for the merge procedure. Looking into the pseudocode of Merge, we can note the following: -* Steps 1 and 2 are constant time. Similarly for steps 5 to 7. This contributes to $O(1)$. -* The copying to left and right array depends on the number of elements and so $O(n)$. -* Steps 8, 9 and 10 are taken to insert the numbers into the sorted array. These are done in total of $n$ times because the final result is $n$ elements in the sorted array. This means that it is also $O(n)$. -* The sub steps inside 8, 9 and 10 are all constant times which is repeated for $n$ times. - -Therefore, the merge step computation time can be written as: - -$$T_{merge}(n) = O(1) + O(n) + O(n)\times(O(1)) = O(n)$$ - -Combining all the timing, we then have the following for the merge sort computation. - -$$T(n) = \begin{cases}O(1), & \text{if } n=1\\ 2T(n/2) + O(n), & \text{if } n > 1\end{cases}$$ - -So now for $n>1$, we have the recurrence relation as follows: - -$$T(n) = 2 T(n/2) + c n$$ - -where $c$ is a constant and $c > 0$. - -We can draw the recurrence tree as shown in the figure below. - -![](/assets/images/week3/mergesort_tree.jpeg) - -Note that at the bottom of the tree, there are $n$ leaves, where $n$ is the number of input in the array. We can also calculate the level at the bottom. At every level, the computation time at each node in the recurrence tree is given by: - -$$\frac{cn}{2^i}$$ - -For example, at $i = 1$, the computation time is - -$$\frac{cn}{2^1} = \frac{cn}{2}$$ - -At the bottom of the tree, we can only $c\times 1$. Therefore, we have the following relationship. - -$$\frac{cn}{2^{i_{bottom}}} = c \times 1$$ - -and we can get - -$$i_{bottom} = \log_2(n)$$ - -This means that the height of the tree is: - -$$h = 1 + \log_2(n)$$ - -And if we sum up the computation time at each leve, we would obtain $c\times n$. For example at level $i = 1$, we have - -$$ cn/2 + cn/2 = cn$$ -and similarly at every level. Therefore, the total computation time is the sum at each level multiplied by the number of level. - -$$T(n) = cn \times (1 + \log_2(n)) = O(n\log(n))$$ - -Here and the subsequent expressions, we always use base of 2 for our logarithmic function. - -Note that the computation time is slower than linear but much faster than quadratic time. This means that merge sort gives a faster computation time as compared to bubble sort and insertion sort and is similar to heapsort. - diff --git a/_Notes/Multiple_Linear_Regression.md b/_Notes/Multiple_Linear_Regression.md deleted file mode 100644 index 3e07c07..0000000 --- a/_Notes/Multiple_Linear_Regression.md +++ /dev/null @@ -1,157 +0,0 @@ ---- -title: Multiple Linear Regression -permalink: /notes/multiple_linear_regression -key: notes-multiple-linear-regression -layout: article -nav_key: Notes -sidebar: - nav: Notes -license: false -aside: - toc: true -show_edit_on_github: false -show_date: false ---- - -## Introduction - -In the previous notes, we only have one independent variable or one feature. In most cases of machine learning, we want to include more than one feature or we want to have a hypothesis that is not simply a straight line. For the first example, we may want to consider not only the floor area but also the storey level to predict the resale price of HDB houses. For the second example, we may want to model the relationship not as a straight line but rather as quadratic. Can we still use linear regression to do these? - -This section discusses how we can include more than one feature and how to model our equation beyond a simple straight line using multiple linear regression. - -## Hypothesis - -Recall that in linear regression, our hypothesis is written as follows. - -$$\hat{y}(x) = \hat{\beta}_0 + \hat{\beta}_1 x$$ - -where $x$ is the only independent variable or feature. In multiple linear regression, we have more than one feature. We will write our hypothesis as follows. - -$$\hat{y}(x) = \hat{\beta}_0 + \hat{\beta}_1 x_1 + \hat{\beta}_2 x_2 + \ldots + \hat{\beta}_n x_n$$ - -In the above hypothesis, we have $n$ features. Note also that we can assume to have $x_0 = 1$ with $\hat{\beta}_0$ as its coefficient. - - -We can write this in terms of a row vector, where the features are written as - -$$\mathbf{X} = \begin{bmatrix} -x_0 & x_1 & \ldots & x_n -\end{bmatrix} \in {\rm I\!R}^{n+1}$$ - -Note that the dimension of the feature is $n+1$ because we have $x_0 = 1$ which is a constant of 1. - -The parameters can be written as follows. - -$$\mathbf{\hat{b}} = \begin{bmatrix} -\hat{\beta}_0 \\ -\hat{\beta}_1 \\ -\ldots \\ -\hat{\beta}_n -\end{bmatrix} \in {\rm I\!R}^{n+1}$$ - -Our system equations for all the data points can now be written as follows. - -$$\hat{y}(x^1) = \hat{\beta}_0 + \hat{\beta}_1 x_1^1 + \hat{\beta}_2 x_2^1 + \ldots + \hat{\beta}_n x_n^1$$ -$$\hat{y}(x^2) = \hat{\beta}_0 + \hat{\beta}_1 x_1^2 + \hat{\beta}_2 x_2^2 + \ldots + \hat{\beta}_n x_n^2$$ -$$\ldots$$ -$$\hat{y}(x^m) = \hat{\beta}_0 + \hat{\beta}_1 x_1^m + \hat{\beta}_2 x_2^m + \ldots + \hat{\beta}_n x_n^m$$ - -In the above equations, the superscript indicate the index for the data points from 1 to $m$, assuming there are $m$ data points. - -To write the hypothesis as a matrix equation we first need to write the features as a matrix for all the data points. - -$$\mathbf{X} = \begin{bmatrix} -1 & x_1^1 & \ldots & x_n^1 \\ -1 & x_1^2 & \ldots & x_n^2 \\ -\ldots & \ldots & \ldots & \ldots \\ -1 & x_1^m & \ldots & x_n^m -\end{bmatrix} \in {\rm I\!R}^{m \times (n+1)}$$ - -with this, we can now write the hypothesis as a matrix multiplication. - -$$\mathbf{\hat{y}} = \mathbf{X} \times \mathbf{\hat{b}}$$ - -Notice that this is the same matrix equation as a simple linear regression. What differs is that $\mathbf{\hat{b}}$ contains more than two parameters. Similarly, the matrix $\mathbf{X}$ is now of dimension $m\times(n+1)$ where $m$ is the number of data points and $n+1$ is the number of parameters. Next, let's see how we can calculate the cost function. - -## Cost Function - -Recall that the cost function is written as follows. - -$$J(\hat{\beta}_0, \hat{\beta}_1) = \frac{1}{2m}\Sigma^m_{i=1}\left(\hat{y}(x^i)-y^i\right)^2$$ - -We can rewrite the square as a multiplication instead and make use of matrix multplication to express it. - -$$J(\hat{\beta}_0, \hat{\beta}_1) = \frac{1}{2m}\Sigma^m_{i=1}\left(\hat{y}(x^i)-y^i\right)\times \left(\hat{y}(x^i)-y^i\right)$$ - -Writing it as matrix multiplication gives us the following. - -$$J(\hat{\beta}_0, \hat{\beta}_1) = \frac{1}{2m}(\mathbf{\hat{y}}-\mathbf{y})^T\times (\mathbf{\hat{y}}-\mathbf{y})$$ - -This equation is exactly the same as the simple linear regression. - -## Gradient Descent - -Recall that the update function for gradient descent algorithm for a linear regression is given as follows. - -$$\hat{\beta}_0 = \hat{\beta}_0 - \alpha \frac{1}{m}\Sigma_{i=1}^m\left(\hat{y}(x^i) - y^i\right)$$ - -$$\hat{\beta}_1 = \hat{\beta}_1 - \alpha \frac{1}{m}\Sigma_{i=1}^m\left(\hat{y}(x^i) - y^i\right)x^i$$ - -In the case of multiple linear regression, we have more than one feature and so we need to differentiate for each $\theta_j$. Doing this will result in a system of equation as follows. - -$$\hat{\beta}_0 = \hat{\beta}_0 - \alpha \frac{1}{m}\Sigma_{i=1}^m\left(\hat{y}(x^i) - y^i\right)x_0^i$$ - -$$\hat{\beta}_1 = \hat{\beta}_1 - \alpha \frac{1}{m}\Sigma_{i=1}^m\left(\hat{y}(x^i) - y^i\right)x_1^i$$ - -$$\hat{\beta}_2 = \hat{\beta}_2 - \alpha \frac{1}{m}\Sigma_{i=1}^m\left(\hat{y}(x^i) - y^i\right)x_2^i$$ - -$$\ldots$$ - -$$\hat{\beta}_n = \hat{\beta}_n - \alpha \frac{1}{m}\Sigma_{i=1}^m\left(\hat{y}(x^i) - y^i\right)x_n^i$$ - -Note that $x_0 = 1$ for all $i$. - -We can now write the gradient descent update function using matrix operations. - -$$\mathbf{\hat{b}} = \mathbf{\hat{b}} - \alpha\frac{1}{m} \mathbf{X}^T \times (\mathbf{\hat{y}} - \mathbf{y})$$ - -Substituting the equation for $\mathbf{\hat{y}}$ gives us the following. - -$$\mathbf{\hat{b}} = \mathbf{\hat{b}} - \alpha\frac{1}{m} \mathbf{X}^T \times (\mathbf{X}\times \mathbf{\hat{b}} - \mathbf{y})$$ - -Again, this is exactly the same as the simple linear regression. - -This means that all our equations have not changed and what we need to do is create the right parameter vector $\mathbf{\hat{b}}$ and the matrix $\mathbf{X}$. Once we constructed these vector and matrix, all the other equations remain the same. - -## Polynomial Model - -There are time that even when there is only one feature we may want to have a hypothesis that is not a straight line. An example of would be if our model is a quadratic equation. We can use multiple linear regression to create hypothesis beyond a straight line. - -Recall that in multiple linear regression, the hypothesis is writen as follows. - -$$\hat{y}(x) = \hat{\beta}_0 + \hat{\beta}_1 x_1 + \hat{\beta}_2 x_2 + \ldots + \hat{\beta}_n x_n$$ - -To have a quadratic hypothesis, we can set the following: - -$$x_1 = x$$ -$$x_2 = x^2$$ - -And so, the whole equation can be written as - -$$\hat{y}(x) = \hat{\beta}_0 + \hat{\beta}_1 x + \hat{\beta}_2 x^2 $$ - -In this case, the matrix for the features becomes as follows. - - -$$\mathbf{X} = \begin{bmatrix} -1 & x^{(1)} & (x^2)^{(1)} \\ -1 & x^{(2)} & (x^2)^{(2)} \\ -\ldots & \ldots \\ -1 & x^{(m)} & (x^2)^{(m)} -\end{bmatrix} \in {\rm I\!R}^{m \times 3}$$ - -In the notation above, we have put the index for the data point inside a bracket to avoid confusion with the power. - -We can genearalize this to any power of polynomial where each power is treated as each feature in the matrix. This means that if we want to to model the data using any other polynomial equation, what we need to do is to transform the $\mathbf{X}$ matrix in such a way that each column in $\mathbf{X}$ represents the right degree of polynomial. Column zero is for $x^0$, column one is for $x^1$, column two is for $x^2$, and similarly all the other columns until we have column n for $x^n$. - -The parameters can be found using the same gradient descent that minimizes the cost function. diff --git a/_Notes/Nested_For_Loop.md b/_Notes/Nested_For_Loop.md new file mode 100644 index 0000000..05e5e07 --- /dev/null +++ b/_Notes/Nested_For_Loop.md @@ -0,0 +1,704 @@ +--- +title: Nested For Loops +permalink: /notes/nested-for +key: notes-nested-for +layout: article +nav_key: Notes +sidebar: + nav: Notes +license: false +aside: + toc: true +show_edit_on_github: false +show_date: false +--- + +## Traversing a Nested List + +In the previous lesson, we introduced the idea that a collection data type like a list can have elements that are also lists. This introduces us to nested data type. There are times that we may need to visit every element in our nested list which require us to use a nested loop. One simple example that requires us to do is, let's say, we want to represent our data differently. In our previous lesson, we have `month_steps` where we store the steps of every day in a week and every week in a month. It was represented as below. + +```python +month_steps_week: list[list[int]] = [[40, 50, 43, 55, 67, 56, 60], + [54, 56, 47, 62, 61, 46, 61], + [52, 56, 63, 58, 62, 66, 62], + [57, 58, 46, 71, 63, 76, 63]] +``` + +Notice, that this list is easy to access if we are interested in the *weekly* data. It's easy to get the steps in the first week or the second week or the third and so on. However, as shown in the previous lesson, it is not easy if we wish to access the data from particular *day*. For example, if we want to display what is my number of steps every Wednesday, we have to slice the data using our previous function that we developped in the last lesson. But what if we represent the data where each *sub-list* is a particular day. This means that the rows are the days and the column and the week. It will be easy to get the steps data for every Wednesday on that month. + +```python +month_steps_day: list[list[int]] = [[40, 54, 52, 57], + [51, 56, 56, 58], + [43, 47, 63, 46], + [55, 62, 58, 71], + [67, 61, 62, 63], + [56, 46, 66, 76], + [60, 61, 62, 63]] +``` + +We still have the same data but now the first row is the number of steps on Sundays. The first Sunday was 40 steps, the second Sunday was 54 steps and so on. Similarly, the second row now is the number of steps on Mondays. In this case, it is simple to get the data on Wednesdays which is on index 3. + +```python +>>> month_steps_day[3] +[55, 62, 58, 71] +``` + +What we did was what is called as a *transpose* operation. In a transpose operation, the data in the rows become the columns and the columns become the rows. In order to do this transpose operation, we will need to visit every element in the nested list and, therefore, require a nested for-loop. Let's see how we can do this transpose operation using a nested for-loop. + +### Traversing the Sub-List + +Let's recall how we can use a for-loop to traverse a list. If we only have a single iterable, we can get the element of that iterable using the following syntax. + +```python +for var in iterable: + # block A + do_something() +``` + +In this way, we can get the sublist of our `month_steps_week` using the following code. + +```python +for week_list in month_steps_week: + print(week_list) +``` + +The output is as follows. + +``` +[40, 50, 43, 55, 67, 56, 60] +[54, 56, 47, 62, 61, 46, 61] +[52, 56, 63, 58, 62, 66, 62] +[57, 58, 46, 71, 63, 76, 63] +``` + +We can see that each list is printed in a separate line. We can also get the index and the item at the same time using `enumerate()` function. + +```python +for row_idx, week_list in enumerate(month_steps_week): + print(f'row: {row_idx}, element: {week_list}') +``` + +The output is shown below. + +``` +row: 0, element: [40, 50, 43, 55, 67, 56, 60] +row: 1, element: [54, 56, 47, 62, 61, 46, 61] +row: 2, element: [52, 56, 63, 58, 62, 66, 62] +row: 3, element: [57, 58, 46, 71, 63, 76, 63] +``` + +### Traversing the Element of the Sub-List + +Now, if we want to get every element in the row, we can iterate over the `week_list` and print the element of that list. + +```python +for row_idx, week_list in enumerate(month_steps_week): + print("---") + for col_idx, element in enumerate(week_list): + print(f'row: {row_idx}, col: {col_idx}, element: {element}') +``` + +The output is shown below. + +``` +--- +row: 0, col: 0, element: 40 +row: 0, col: 1, element: 50 +row: 0, col: 2, element: 43 +row: 0, col: 3, element: 55 +row: 0, col: 4, element: 67 +row: 0, col: 5, element: 56 +row: 0, col: 6, element: 60 +--- +row: 1, col: 0, element: 54 +row: 1, col: 1, element: 56 +row: 1, col: 2, element: 47 +row: 1, col: 3, element: 62 +row: 1, col: 4, element: 61 +row: 1, col: 5, element: 46 +row: 1, col: 6, element: 61 +--- +row: 2, col: 0, element: 52 +row: 2, col: 1, element: 56 +row: 2, col: 2, element: 63 +row: 2, col: 3, element: 58 +row: 2, col: 4, element: 62 +row: 2, col: 5, element: 66 +row: 2, col: 6, element: 62 +--- +row: 3, col: 0, element: 57 +row: 3, col: 1, element: 58 +row: 3, col: 2, element: 46 +row: 3, col: 3, element: 71 +row: 3, col: 4, element: 63 +row: 3, col: 5, element: 76 +row: 3, col: 6, element: 63 +``` + +We have printed every element in our nested list. + +It's worth to relook at our code and examine it closely. + +```python +for row_idx, week_list in enumerate(month_steps_week): + # Beginning of Block A for "outer-loop" + print("---") + for col_idx, element in enumerate(week_list): + print(f'row: {row_idx}, col: {col_idx}, element: {element}') + # Ending of Block A for "outer-loop" +``` + +Recall that block A is a block of Code inside the for-loop that is repeated for every element in the iterable. We have put a comment above to indicate all the codes below the first `for` statement is actually a block of code that is repeated for every item in our `month_steps_week`. We call our first for-loop in the first line as our "outer-loop". Since we have four weeks, this block A for the "outer-loop" will be repeated four times. There are two lines of code to be executed. The first is to print a three dash line and the second one is another for loop. We will call this for loop our "inner-loop" since it is inside the other for-loop. Notice that since we repeat block A four times for our "outer-loop", Python executes our `print('---')` four times as well. This can be observed in the output above. You will see there are only four `---` in the output. + +The second `for` statement is what we call as our "inner-loop". This second `for` statement is part of block A of the "outer-loop". This means that this "inner-loop" will be repeated four times. Each time, Python will go through ever element in the "inner-loop". In this "inner-loop" we go through `week_list` which is our sub-list in every row. There are seven days in each week and so the "inner-loop" will repeat for seven times. We can observe this in our output for the printed text between the two `---` lines. + +``` +--- +row: 0, col: 0, element: 40 +row: 0, col: 1, element: 50 +row: 0, col: 2, element: 43 +row: 0, col: 3, element: 55 +row: 0, col: 4, element: 67 +row: 0, col: 5, element: 56 +row: 0, col: 6, element: 60 +--- +... more output below +``` + +Notice that there are seven elements being printed from col 0 to col 6. All these elements are from row 0. We can see similar output for row 1 with another seven elements being printed. + +The *second* `print()` statement, where we print out the row, col and the element, is located inside block A of the "inner-loop". This means that this print statement is repeated seven times by the inner loop. And since the inner-loop is repeated four times, we have a total of $4\times 7 = 28$ print out in the screen due to this second print statement. + +```python +for row_idx, week_list in enumerate(month_steps_week): + # Beginning of Block A for "outer-loop" + print("---") + for col_idx, element in enumerate(week_list): + ## Beginning of Block A for "inner-loop" + print(f'row: {row_idx}, col: {col_idx}, element: {element}') + ## Ending of Block A for "inner-loop" + # Ending of Block A for "outer-loop" +``` + +We have added two comments to indicate the beginning and ending of Block A for the "inner-loop". You should try to run through the code in Python Tutor and verify the steps. + + + +Notice that the program counter repeats the inner-loop for four times. This results in the second print statement to be executed 28 times. + +## Using Print to Debug Nested List Code + +Dealing with Nested For-Loop and Nested List can be confusing. One simple way to check your way as you code is to print the values of the state of the loop. We can do this by inserting `print()` statement and simply print the variable of the loop. + +```python +for var in iterable: + # block A + print(var) # do this to check your loop + do_something() +``` + +Inside your block A of your loop, you should immediately print out the `var` of your iterable to check our loop. There are a few things that you can check. +- See if `var` is what you expected to be the element of the iterable. +- Check the data type of `var` +- See if `var` starts and ends as you expected + +Similarly, when there are more than one loops, it is good to continue checking the state of your loop. + +```python +for outer_var in outer_iterable: + # block A of outer loop + print(outer_var) # do this to check your outer loop + print("marking") + for inner_var in inner_iterable: + # block A of inner loop + print(inner_var) + do_something() +``` + +Sometimes it is useful to print some *marking* to indicate when we are starting the inner loop. This is especially when you have multiple lines of code in both your inner and outer loop. + + +With this, we are ready to tacke the problem of transposing our data. + +## Transposing Your List of Lists + +Let's apply what we have learnt on traversing nested list to create a transpose of our list of lists. Let's use PCDIT framework to work it out. + +### (P)roblem Definition + +In this problem, we are given a list of lists and we would like to output another list of lists which is a transpose of the input list. Let's write it down. + +``` +Input: + - array: list of lists: input list of lists to be transposed. +Output: + - array: list of lists: output list which is a transposed of the input array. +Problem Statement + Given an input list of lists, the function should return a transposed list of lists. + A transposed list of lists has the same data of the input lists, + where the row data becomes the column and the column data becomes the row data. +``` + +### Concrete(C)ases + +Let's work out some concrete cases. Instead of dealing with `month_steps_week`, we will use a simpler list of lists as shown below. + +```python +input_array: list[list[int]] = [[1, 2, 3, 4], + [5, 6, 7, 8], + [9, 10, 11, 12]] +``` + +What we would like to have on the output is as follows. + +```python +output_array: list[list[int]] = [[1, 5, 9], + [2, 6, 10], + [3, 7, 11], + [4, 8, 12]] +``` + +Now, we need to work it out how to get the output from the input step by step as if we are the machine. Our plan is to construct the output one at a time. First, we will create the first sub-list, i.e. `[1, 5, 9]`. When this is done, we will insert this sublist into the output list. To begin, we will create an empty list. + +```python +output_array: list[list[int]] = [] +``` + +Next, we need to create another empty list for the first sublist and start filling up the elements. + +```python +sublist: list[int] = [] +``` + +How do we get the element? We need to get it from the `input_array`. This is where we need to traverse the input list of lists to get the element. We need to get the element `1`, `5` and `9`. These three are at different sub-lists in the input array. So let's write down the step and what we get. + +``` +- Go to the first sublist +- Get the first element +- Add this to the sublist + +->[1, 2, 3, 4] + ^ + | +``` + +Notice in the above, we have two arrays. The "horizontal" arrow and the "vertical" arrow. The horizontal arrow is pointing to the first sublist. In this sublist pointed by the horizontal arrow, we have the second arrow pointing to which element we want to get, which is the vertical arrow. At the moment it points to the first element. + +After these steps, we get the following for our sublist. + +```python +sublist = [1] +``` + +Once we get `1`, we need to go to the second sub-list to get `5`. To do this, we move our "horizontal" arrow down to the second sub-list. We can keep the position of the "vertical" arrow since `5` is still located at the first element. + +``` +- Go to the second sublist +- Get the first element +- Add this to the sublist + + [1, 2, 3, 4] +->[5, 6, 7, 8] + ^ + | +``` + +After we add `5` to the sublist, we have the following. + +```python +sublist = [1, 5] +``` + +Now, we do the same by going to the third sublist. + +``` +- Go to the third sublist +- Get the first element +- Add this to the sublist + + [1, 2, 3, 4] + [5, 6, 7, 8] +->[9, 10, 11, 12] + ^ + | +``` + +and after we add `9`, we get the following. + +```python +sublist = [1, 5, 9] +``` + +We are done in getting the first sublist in the `output_array`. So we can add this sublist into the output list. + +```python +output_array = [[1, 5, 9]] +``` + +Notice, that `output_array` is a list of one element. This one element, however, is a list with three elements. It is a list of lists. + +We have finished with the first row in the output. Let's construct the second row to get `[2, 6, 10]`. To do this, we need to move the "vertical" arrow to the **second** element in the sublist. We also need to reset the "horizontal" arrow back to the first sublist. + +``` +- Go to the *first* sublist +- Get the *second* element +- Add this to the sublist + +->[1, 2, 3, 4] + ^ + | + [5, 6, 7, 8] + [9, 10, 11, 12] +``` + +With this, we get `2` and add it into our sublist. Recall that we want to construct a new row, so we need to create a new sublist for the new row. + +```python +sublist = [2] +``` + +We can then move to the second row to get `6`. + +``` +- Go to the *first* sublist +- Get the *second* element +- Add this to the sublist + + [1, 2, 3, 4] +->[5, 6, 7, 8] + ^ + | + [9, 10, 11, 12] +``` + +And we add this to sublist. + +```python +sublist = [2, 6] +``` + +Lastly, we do the same with the third row. + +``` +- Go to the *first* sublist +- Get the *second* element +- Add this to the sublist + + [1, 2, 3, 4] + [5, 6, 7, 8] +->[9, 10, 11, 12] + ^ + | +``` + +And we end up in the following sublist. + +```python +sublist = [2, 6, 10] +``` + +We are then ready to insert this into the `output_array`. + +```python +output_array = [[1, 5, 9], + [2, 6, 10]] +``` + +I hope by now, you can see the pattern of how we are goint to get the final output. This pattern is important as we will write it down in the next (D)esign of Algorithm step. What we will do is to move the "vertical" arrow to the third position and reset the "horizontal" arrow back to the first row in the input list. We will then repeat the same steps. As you can see by now that we are doing a few steps that are being repeated again and again. This structure is our **iterative** structure. Let's write down a general steps in the next section. + +### (D)esign of Algorithm + +Let's try to generalize the steps in the previous Concrete (C)ases. One thing we have identified is that we need to repeat some steps again and again. This involves iterative structure. How many times do we need to repeat them? For each output row, we need to move the "horizontal" arrow down three times which is the number of row in the input list. Moreover, we need to keep on adding the element to the `sublist` four times. We did this by moving the "vertical" arrow to the right four times which is the number of column in the input list of lists. + +Let's start by creating an empty output array. + +``` +1. create an empty output_array +``` + +After this step, we actually have to add the sublist into this `output_array` four times, which is the number we move the "vertical" arrow. This is also the number of columns in the input list of lists. Let's write the big steps in the following way. + +``` +1. create an empty output_array +2. for the number of columns in the input_array + 2.1 create output sublist from the input_array + 2.2 add sublist into output_array +``` + +The question is how we create the output sublist. We need to expand step 2.1 into several steps. Let's recall what we did when we create the output sublist. + +``` +Creating output sublist +1. create an empty sublist +2. for the number of rows in the input_array + 2.1 add the element into the output sublist +``` + +Recall that we move the horizontal arrow three times which is the number of rows in the input array. Every time we move down the horizontal arrow, we add the element pointed by the arrow into our output sublist. We can then combine and insert these steps into our overall steps. + +``` +1. create an empty output_array +2. for the number of columns in the input_array + 2.1 create an empty sublist + 2.2 for the number of rows in the input_array + 2.2.1 add the element into the output sublist + 2.3 add sublist into output_array +``` + +Maybe, we should remind ourselves that we need to move our arrows as well. So, we will add a few steps to remind us to do this. Let's start with the horizontal arrow. The horizontal arrow moves down for the number of rows in the input array. So we will add this after steps 2.2.1. + +``` +1. create an empty output_array +2. for the number of columns in the input_array + 2.1 create an empty sublist + 2.2 for the number of rows in the input_array + 2.2.1 add the element into the output sublist + 2.2.2 move the horizontal arrow down by one + 2.3 add sublist into output_array +``` + +On the other hand, the vertical arrow moves to the right after we finish adding the sublist into the output_array. This means that we can add this step after step 2.3. + +``` +1. create an empty output_array +2. for the number of columns in the input_array + 2.1 create an empty sublist + 2.2 for the number of rows in the input_array + 2.2.1 add the element into the output sublist + 2.2.2 move the horizontal arrow down by one + 2.3 add sublist into output_array + 2.4 move the vertical arrow to the right by one +``` + +Let's try to implement these steps that we have written. + +### (I)mplementation and (T)est + +We will create a python script called `transpose.py` to implement and test our steps. Let's start by writing the function header and some code to test it. + +```python +def transpose(input_array: list[list[int]]) -> list[list[int]]: + output_array: list[list[int]] = [] + return output_array + +input_array: list[list[int]] = [[1, 2, 3, 4], + [5, 6, 7, 8], + [9, 10, 11, 12]] +output: list[list[int]] = transpose(input_array) +print(output) +``` + +In the code above, we have done step 1 which is to create an empty output array. Currently, the function returns an empty list. + +Running mypy and python shows the following output. + +``` +$ mypy transpose.py +Success: no issues found in 1 source file +$ python transpose.py +[] +``` + +Step 2 is to iterate for the number of columns in the input array. We will use the `range()` function so that we can get both the index. On top of that, we will first calculate the number of rows and columns in the input array. In the code below, we remove the other parts of the code and focus only on the function definition. + +```python +def transpose(input_array: list[list[int]]) -> list[list[int]]: + output_array: list[list[int]] = [] + n_rows: int = len(input_array) + n_cols: int = len(input_array[0]) + for v_arrow in range(n_cols): + print(v_arrow) + return output_array +``` + +Notice that we added `n_rows` and `n_cols` after creating an empty array. The number of rows is the number of sublist and, therefore, we can get it from `len(input_array)`. On the other hand, the number of columns is the number of element *inside the sublist*. Since all the sublist has the same length, we can get the number of columns using `len(input_array[0])`. + +We have also named the index as `v_arrow` which stand for our vertical arrow in the Concrete (C)ases example above. We used the vertical arrow to point to which element in the sublist that we are working at now. The vertical arrow runs over the element across the *column*. Running the script gives the following output. + +``` +0 +1 +2 +3 +[] +``` + +We can ignore the last empty list since we still return the empty `output_array`. However, we can see that vertical arrow iterates from 0 (most left) to 3 (most right) in the input array. + +With this, it seems we have implemented step 2 and we are ready to implement the steps under this outer iteration. Step 2.1 creates another empty array but this time it is for the rows in the output array. Since the rows in the output array have the same number as the columns in the input array, we iterate four times to create the empty lists. + +```python +def transpose(input_array: list[list[int]]) -> list[list[int]]: + output_array: list[list[int]] = [] + n_rows: int = len(input_array) + n_cols: int = len(input_array[0]) + for v_arrow in range(n_cols): + output_row: list[int] = [] + print(output_row) + return output_array +``` + +We have added the line `output_row` and print it out. Since these two lines are inside the outer iteration loop which is iterated four times, we should expect the empty list to be printed four times. + +The output is as follows. + +``` +[] +[] +[] +[] +[] +``` + +Note that there are five empty list where the first four comes from the print statement inside the iteration and the last one comes from printing the final output array. + +Now, we can do step 2.2 which is to iterate over all the rows in the input array and get the elements. + +``` +2.2 for the number of rows in the input_array + 2.2.1 add the element into the output sublist + 2.2.2 move the horizontal arrow down by one +``` + +We can implement Step 2.2 including 2.2.2 using the usual `for-in` statement. + +```python + +def transpose(input_array: list[list[int]]) -> list[list[int]]: + output_array: list[list[int]] = [] + n_rows: int = len(input_array) + n_cols: int = len(input_array[0]) + for v_arrow in range(n_cols): + output_row: list[int] = [] + for h_arrow in range(n_rows): + output_row.append(input_array[h_arrow][v_arrow]) + print(output_row) + return output_array +``` + +We have named the variable in the inner loop as `h_arrow` which stands for the horizontal arrow in our Concrete (C)ases previously. This arrow points to which row in the input list that we are currently working on. This arrow iterates over the number of rows in the input. This number of rows in the input is the same as the number of columns in the output. This is how we assemble the output array's sublist. We added the elements into the `output_row` list. + +Which element that we add? We add the element from `input_array` pointed by the the two arrows. Recall that the row is pointed by the `h_arrow` and the column is pointed by `v_arrow`. Since every row is a sublist which is the element of the `input_array`, we access each sublist using `input_array[h_arrow]`. This gives us a list in the row. We then need to access the element at a particular column pointed by the `v_arrow`. Therefore, our code to get the element is given by `input_array[h_arrow][v_arrow]`. This element is added into `output_row` using `output_row.append()` method. + +We added a single print statement to show `output_row`. Let's see the output. + +``` +[1] +[1, 5] +[1, 5, 9] +[2] +[2, 6] +[2, 6, 10] +[3] +[3, 7] +[3, 7, 11] +[4] +[4, 8] +[4, 8, 12] +[] +``` + +We can see that it first added `1` into the `output_row` and then `5` and then `9`. Once it has three elements, it creates a new list and added `2` for the second row in the output. We repeat these four times which is the number of columns in the input or the number of rows in the output. Comparing these sublist with out Concrete (C)ases example, we can see that we get the output rows correct. Recall the following expected output in Concrete (C)ases. + +```python +output_array: list[list[int]] = [[1, 5, 9], + [2, 6, 10], + [3, 7, 11], + [4, 8, 12]] +``` + +We can see all the rows needed to assemble the output array. Now, what we need to do is to implement step 2.3 which is to add the sublist into the output array. + +```python +def transpose(input_array: list[list[int]]) -> list[list[int]]: + output_array: list[list[int]] = [] + n_rows: int = len(input_array) + n_cols: int = len(input_array[0]) + for v_arrow in range(n_cols): + output_row: list[int] = [] + for h_arrow in range(n_rows): + output_row.append(input_array[h_arrow][v_arrow]) + output_array.append(output_row) + print(output_array) + return output_array +``` + +We added step 2.3 in the same indentation as 2.1 and 2.2 code. This is because we want to add the sublist only after we finish adding the elements into the sublist, which is step 2.2 iteration. We added print statement to see how the output array looks like at each iteration. Note that Step 2.3 is still placed under the outer loop and so we should see the print statement to be executed four times which is the number of columns in the input array. + +``` +[[1, 5, 9]] +[[1, 5, 9], [2, 6, 10]] +[[1, 5, 9], [2, 6, 10], [3, 7, 11]] +[[1, 5, 9], [2, 6, 10], [3, 7, 11], [4, 8, 12]] +[[1, 5, 9], [2, 6, 10], [3, 7, 11], [4, 8, 12]] +``` + +We have five lines where the last two lines are the same. The reason is that the last print comes from printing the output array outside of the function definition. In the output above, we can see how the sublists are being added at every iteration. + +We got the final output array correctly and we can remove the print statement inside the function definition. + +```python +def transpose(input_array: list[list[int]]) -> list[list[int]]: + output_array: list[List[int]] = [] + n_rows: int = len(input_array) + n_cols: int = len(input_array[0]) + for v_arrow in range(n_cols): + output_row: list[int] = [] + for h_arrow in range(n_rows): + output_row.append(input_array[h_arrow][v_arrow]) + output_array.append(output_row) + return output_array +``` + +Running mypy and python on the script file now gives the following output. + +``` +$ mypy transpose.py +Success: no issues found in 1 source file +$ python transpose.py +[[1, 5, 9], [2, 6, 10], [3, 7, 11], [4, 8, 12]] +``` + +You can execute the code step by step in Python Tutor below. We have removed the type annotation to make the environment diagram cleaner. + + + +## Function Composition Use Case + +At the beginning of this lesson, we mentioned that getting the number of steps for every Wednesdays in a week was not easy and we need to use our `slice_2d` function. But if we transpose the data, it is easier to get the Wednesdays' steps data. Let's create a function that takes in `month_steps` and makes use of our transpose function to get the number of steps on a particular day in the month. + +We want to be able to pass `month_steps` and get the number of steps for Wednesdays in that month. + +```python +month_steps: list[list[int]] = [[40, 50, 43, 55, 67, 56, 60], + [54, 56, 47, 62, 61, 46, 61], + [52, 56, 63, 58, 62, 66, 62], + [57, 58, 46, 71, 63, 76, 63]] +wednesdays: list[int] = steps_month_for_day(month_steps, 3) +``` + +We created a function called `steps_month_for_day()` which takes in two arguments. The first is the list of lists containing all the steps in a month. This list has the number of steps for each week in its rows. The second argument is the index of the day we are interested in. In the above code, we put in `3` for Wednesday since we start with Sunday which is index `0`. + +We can then write this function as follows. + +```python +def steps_month_for_day(month_steps: list[list[int]], index: int) -> list[int]: + steps_on_days: list[list[int]] = transpose(month_steps) + output: list[int] = steps_on_days[index] + return output +``` + +In the above code, we first transpose the input array. Once it is transposed, we can get the number of steps on every Wednesday using a simple slicing. + +Running mypy and python on the script gives us the following. + +``` +$ mypy steps_month_for_day.py +Success: no issues found in 1 source file +$ python steps_month_for_day.py +[55, 62, 58, 71] +``` + +## Summary + +In this lesson, we showed how we can nest an iteration structure inside another iteration structure. We worked on transpose operation and applied some print statement along the way during the implementation of the code. Nested structure is not limited to only two for-loops. It can be more than two. However, the more nested it is, the more complicated and harder to debug. It is best to keep the loop as shallow as possible for easy debugging. \ No newline at end of file diff --git a/_Notes/Nested_List.md b/_Notes/Nested_List.md new file mode 100644 index 0000000..9237a68 --- /dev/null +++ b/_Notes/Nested_List.md @@ -0,0 +1,684 @@ +--- +title: Nested List +permalink: /notes/nested-list +key: notes-nested-list +layout: article +nav_key: Notes +sidebar: + nav: Notes +license: false +aside: + toc: true +show_edit_on_github: false +show_date: false +--- + +## Nested Data Structure + +So far, we have worked on two kinds of collection data types. The first one is tuple and the second one is a list. In each of those lessons, we showed how to contain various items in a single collection. This collection, however, is linear or one dimensional. In our last example, we have a list of steps in a week. + +```python +my_steps: list[int] = [40, 50, 43, 55, 67, 56, 60] +``` + +What if we want to contain the list of steps in a month? We can continue adding the items to have a list of 30 or 31 elements. To go to the second or third week, however, we have to calculate what is the starting index of that week. For example, the first week starts from 0 to 6. The second week, on the other hand, will be from 7 to 13. The third week will be from 14 to 20 and so on. This means that when we work on the second or third or fourth week, the index of the maximum step is no longer from 0 to 6 and we are unable to make use of the function `get_name_day()` anymore. We have to transform it to 0 to 6 range before we can use that function. It's not that difficult. But there is another way we can organize our data. + +We can organize our data as a list in a list. This is what we call as nested data structure. This means that we have one list to represent the month where each item is to represent each week. This representation for each week can be another list. Let's see how we can create and work on this nested data structure. + +## Creating a Nested List + +Let's say, we have data for four weeks exercises. Each week is contained in a list. + +```python +week_1: list[int] = [40, 50, 43, 55, 67, 56, 60] +week_2: list[int] = [54, 56, 47, 62, 61, 46, 61] +week_3: list[int] = [52, 56, 63, 58, 62, 66, 62] +week_4: list[int] = [57, 58, 46, 71, 63, 76, 63] +``` + +We can then store these data in another list for a month. + +```python +month_steps: list[list[int]] = [week_1, week_2, week_3, week_4] +``` + +Notice that this list has **four elements**. Each element is a **list** data type. We can check it's length and type using the normal list operations. + +```python +print(type(month_steps), len(month_steps)) +print(type(month_steps[0]), len(month_steps[0])) +``` + +The first line prints the type of the `month_steps` list and its length. The second line prints the type of the first element of `month_steps` which is `week_1` and its type. The output is shown below. + +``` + 4 + 7 +``` + +We can see that `month_steps` has four elements which are the four week lists. On the other hand, the first element of `month_steps` is also a list but this list has seven elements which is the number of steps for each day in that week. + +Another thing that we introduced here is the type annotatin for nested list. See that we defined it in the following way. + +```python +month_steps: list[list[int]] = [week_1, week_2, week_3, week_4] +``` + +We used the usual `list[element_type]` for annotating `month_steps`. The only thing is that the element type for this list is another list with `int` elements, i.e. `list[int]`. + +We need not create a variable and directly create a single nested list literal as shown below. + +```python +month_steps: list[list[int]] = [[40, 50, 43, 55, 67, 56, 60], + [54, 56, 47, 62, 61, 46, 61], + [52, 56, 63, 58, 62, 66, 62], + [57, 58, 46, 71, 63, 76, 63]] +``` + +The above list creates the same nested list as before. You can actually write everythign in a single line. However, it is easier for us to separate the different week in a separate lines in creating the above list. + +## Environment Diagram of a Nested List + +It is important for us to see what the environment diagram looks like for a nested data structure like the above. Previously, we showed the environment diagram of a single list. See below the arrow on the right hand side that points to the list object. + + + +Notice that the list object contains the int immutable object. Now compares this with the environment diagram of a nested list. + + + +Notice that we have more *arrows* in this diagram. The name `month_steps` has an arrow pointing to a list of *four* elements. The element of this list is not an integer but rather another *arrow* which points to different list objects. Understanding this environment diagram helps us to understand what happens when we copy or access elements in the array. Let's look at some basic operations of a nested list. + +## Basic Operations with Nested Lists + +### Accessing an Element + +The first basic operation is accessing an element either to read the value or to modify the value. Notice that `month_steps` is a list data type where each element is another list. See the type annotation of this variable. + +```python +month_steps: list[list[int]] = [[40, 50, 43, 55, 67, 56, 60], + [54, 56, 47, 62, 61, 46, 61], + [52, 56, 63, 58, 62, 66, 62], + [57, 58, 46, 71, 63, 76, 63]] +``` + +We have also seen the environment diagram in the previous section. The environment diagram shows that `month_steps` is actually just a list of four elements. This means that we can use the usual get item operator or the square bracket operator to get the item of its element. + +```python +>>> month_steps[0] +[40, 50, 43, 55, 67, 56, 60] +>>> month_steps[1] +[54, 56, 47, 62, 61, 46, 61] +``` + +We can also check the type of this element using the `type()` function. + +```python +>>> type(month_steps[0]) + +``` + +In order to get the number of steps in day one of week one, we need to access the list of list. Let's show how to do this in steps. + +```python +>>> month_steps[0] +[40, 50, 43, 55, 67, 56, 60] +>>> month_steps[0][0] +40 +``` + +Notice that we used two get item operators here. The first one is to get the element of `month_steps` which is the first week, i.e. `month_steps[0]`. The second get item operator is to get the first element of that week which is the step of day one in week one. To make it clear, you can actually assign the first week to a variable first. + +```python +>>> week_1: list[int] = month_steps[0] +>>> week_1 +[40, 50, 43, 55, 67, 56, 60] +>>> week_1[0] +40 +``` + +Notice that this `week_1[0]` is the same as `month_steps[0][0]` because `week_1 = month_steps[0]`. Similarly, if we wish to access the third day of week four, we can type the following code. + +```python +>>> month_steps[3][2] +46 +``` + +Notice that week four is index 3 in the list since our indexing starts from 0. Similarly, our day three is index 2. + +You can also modify the element using the get item operator and the assignment operator as usual. For example, you can modify the value of third day in the fourth week as follows. + +```python +>>> month_steps[3][2] +46 +>>> month_steps[3][2] = 57 +>>> month_steps[3][2] +57 +>>> month_steps +[[40, 50, 43, 55, 67, 56, 60], +[54, 56, 47, 62, 61, 46, 61], +[52, 56, 63, 58, 62, 66, 62], +[57, 58, 57, 71, 63, 76, 63]] +``` + +### Slicing + +Similar to a single list, you can also slice a nested list. What you need to remember is that they are just *a list where the *. This means that you need to decide how the list is to be sliced. Let's start with the simplest task of slicing the weeks in `month_steps`. Let's say, we only want to get the first two weeks. We can type the following. + +```python +>>> month_steps: list[list[int]] = [[40, 50, 43, 55, 67, 56, 60], + [54, 56, 47, 62, 61, 46, 61], + [52, 56, 63, 58, 62, 66, 62], + [57, 58, 46, 71, 63, 76, 63]] +>>> month_steps[:2] +[[40, 50, 43, 55, 67, 56, 60], + [54, 56, 47, 62, 61, 46, 61]] +``` + +Recall that `month_steps` has four elements where each element is a list. In order for us to get the first two lists, we slice `month_steps[:2]`. We can also get the second week to the fourth week in the following way. + +```python +>>> month_steps[1:] +[[54, 56, 47, 62, 61, 46, 61], + [52, 56, 63, 58, 62, 66, 62], + [57, 58, 46, 71, 63, 76, 63]] +``` + +What is not possible is to get the steps in the middle of the weeks for all the weeks. There are ways of doing this using `numpy` library when the data type is a numpy array. We can slice the rows easily because each row is just an element in a list. The columns, however, is different. They are list in a list. There is no easy and simple ways of slicing the *columns*. We will come back to this problem and create a function to solve this at the end of the lesson. + + +### Adding and Removing Elements + +Similarly, adding and removing can be done if we recall that nested list is just a list inside a list. For example, we can add another week into the list. + +```python +>>> month_steps.append([53, 52, 51, 56, 67, 45, 47]) +>>> month_steps +[[40, 50, 43, 55, 67, 56, 60], + [54, 56, 47, 62, 61, 46, 61], + [52, 56, 63, 58, 62, 66, 62], + [57, 58, 46, 71, 63, 76, 63], + [53, 52, 51, 56, 67, 45, 47]] +``` + +Notice that the data type of the item in the `append()` argument is a **list**. What if you append an integer instead of a list? + +```python +>>> month_steps.append(100) +>>> month_steps +[[40, 50, 43, 55, 67, 56, 60], + [54, 56, 47, 62, 61, 46, 61], + [52, 56, 63, 58, 62, 66, 62], + [57, 58, 46, 71, 63, 76, 63], + [53, 52, 51, 56, 67, 45, 47], + 100] +``` + +Python will basically just add the `int` item as the last element in the list. Recall that Python allows multiple data types in a single list. However, when you run this code using `mypy`, it will throw an error. The reason is that previously, we annotated `month_steps` as `list[list[int]]`. Try running the file `month_steps_error.py` inside `lesson08` folder with `mypy` and check the error message. + +``` +$ mypy month_steps_error.py +month_steps_error.py:9: error: Argument 1 to "append" of "list" has incompatible type "int"; expected "List[int]" [arg-type] +Found 1 error in 1 file (checked 1 source file) +``` + +We can remove or delete the element in the usual way either using `pop()` or the `del` operator. + +```python +>>> del month_steps[-1] +>>> month_steps +[[40, 50, 43, 55, 67, 56, 60], + [54, 56, 47, 62, 61, 46, 61], + [52, 56, 63, 58, 62, 66, 62], + [57, 58, 46, 71, 63, 76, 63], + [53, 52, 51, 56, 67, 45, 47]] +``` + +Notice that the `100` item has been deleted from the list. Adding something as another row in the list is easy because it is just another item in the `month_steps` list. However, adding something in a *column* is not straight forward. If we just want to add or remove an item in one of the sub-list, we just need to access that sub-list first and use the `list` operations to add the item. Let's say, we want to remove the last element in week 1. + +We can either use `del` on the first sub-list. We access the first sub-list using `month_steps[0]`. +```python +>>> del month_steps[0][-1] +``` + +Or, we can also pop that item out from the first sub-list. + +```python +>>> month_steps[0].pop() +60 +``` + +We can add the item back in using `append()`. + +```python +>>> month_steps[0].append(60) +``` + +Notice that in all these operations, the key idea is to access the *sub-list* `month_steps[0]`. + +## Copying Nested List and Aliasing Issue + +Another important aspect of nested list is the effect of aliasing. Let's recreate `month_steps` using four lists as follows. + +```python +week_1: list[int] = [40, 50, 43, 55, 67, 56, 60] +week_2: list[int] = [54, 56, 47, 62, 61, 46, 61] +week_3: list[int] = [52, 56, 63, 58, 62, 66, 62] +week_4: list[int] = [57, 58, 46, 71, 63, 76, 63] + +month_steps: list[list[int]] = [week_1, week_2, week_3, week_4] +``` + +What if we modify one of the sub-list such as `week_2`. Let's say, we remove the last element of `week_2`. What will happen to month_steps? + +```python +del week_2[-1] +print(month_steps) +``` + +The output is shown below. + +``` +[[40, 50, 43, 55, 67, 56, 60], + [54, 56, 47, 62, 61, 46], + [52, 56, 63, 58, 62, 66, 62], + [57, 58, 46, 71, 63, 76, 63]] +``` + +Notice that the last element in the second sub-list has been removed. It is important to stop here. Recall that we did not apply `del` on `month_steps` but only on `week_2`. However, when `week_2` is modified, the effect is seen as well on `month_steps`. We can understand this by looking into the environment diagram. + + + +Notice that both `month_steps` and `week_2` points to the same list object in the environment. This is why when we modify `week_2`, we also modify `month_steps`. Similarly, when we modify `month_steps`, we will see that `week_2` is modified. Let's add back the element, but this time, we will add `month_steps`. + +```python +month_steps[1].append(61) +print(week_2) +``` + +The output is shown below. + +``` +[54, 56, 47, 62, 61, 46, 61] +``` + +Notice that `61` is back into `week_2`. Both `week_2` and `month_steps`'s second sub-list points to the same object. + +This has important consequences when we slice a nested list. Let's say, we create a new list containing the first two weeks. + +```python +first_two_weeks: list[list[int]] = month_steps[:2] +``` + +The environment diagram is shown below. + + + +Notice that `first_two_weeks` has only two elements. These two elements points to the same objects as the first two sub-lists in `month_steps`, which are the same objects pointed by `week_1` and `week_2` as well. This is what happens when we do a **shallow copy** of a nested list. The first level element of the list is copied but not the deeper levels. Notice that the `first_two_weeks` copied the element of `month_steps`. The two elements copied is the *arrows* pointing to `week_1` and `week_2`. However, it does not make a copy of `week_1` and `week_2`. They still point to the same object. + +What if we want to create a new copy of the deeper level items as well? Python provides a `deepcopy()` function from the `copy` library. + +```python +import copy + +first_two_weeks: list[list[int]] = copy.deepcopy(month_steps[:2]) +``` + +The environment diagram is shown below. + + + +Notice that now, `first_two_weeks` list elements do not point to the same objects as `week_1` and `week_2`. They point to two new list objects. The function `deepcopy()` ensures that you do copy all the objects at all levels. + + +## Slicing Any Part of The List + +We mentioned previously that it is easy to slice along the rows of the nested list but it is not so straight forward to slice along the columns. Another Python package called `numpy` makes this operation easy when dealing with arrays and matrices. However, it is also a good opportunity to solve this problem ourselves. We will apply PCDIT and create a function to solve this problem. + +### (P)roblem Definition + +Let's start by asking what is our problem statement. In order to do this, we need to identify the input and output and write a summary of our problem statement. What we want is that given the starting and ending indices of the rows and the columns, the function should return a subset of the nested list from the original list. + +``` +Input: + - array: list of list with m x n dimension + - row_start: int: starting index of the row in the nested list + - row_end: int: ending index (exclusive) of the row in the nested list + - row_step: int: step size of the row to slice in the nested list + - col_start: int: starting index of the column in the nested list + - col_end: int: ending index (exclusive) of the column in the nested list + - col_step: int: step size of the column to slice in the nested list + +Output: + - list of list sliced from the input + +Problem Statement: + Given a list of list and the slicing indices for the rows and the columns, + return the sliced list of list. +``` + +Notice that we have indicated the data type of the indices as `int`. At the same time, we did not indicate the data type of the element of the list of list as this can be any type. We also assume that the input list has `m` rows and `n` columns. To make it simple, we can assume that each row has the same number of columns. This assumption can be removed later on during the implementation but for simplicity, we will put it here for now. + +### Concrete (C)ases + +Let's work on some concrete cases and see how we can obtain the output. Let's start with a simple list of list. + +```python +input_array = [[1, 2, 3, 4], + [5, 6, 7, 8], + [9, 10, 11, 12], + [13, 14, 15, 16]] +``` + +We will specify a few possible input slice indices. The simplest case would be to get the same list again. This means + +```python +row_start = 0 +row_end = 4 +row_step = 1 +col_start = 0 +col_end = 4 +col_step = 1 +``` + +We start from the first row to the last. The index of the last row is 3 since we only have four rows. But since the ending index is exclusive, we set it as 4. Similarly for the columns. So how can we obtain the output array? + +We can start with an empty output array. + +```python +output_array = [] +``` + +What we will do is that we will go through the rows and get the elements from the sub-list according to the column indices. Let's start with the first row with index 0, i.e. `row_start = 0`. + +```python +current_row_index = 0 +input_row_list = input_array[current_row_index] +``` + +This gives us the following input row list. + +```python +input_row_list = [1, 2, 3, 4] +``` + +Once we got the list in our current row, we can use the indices for the column to slice it. Since we slice from column 0 to 4 (exclusive), we will get the same list for the sliced row list. + +```python +sliced_row_list = [1, 2, 3, 4] +``` + +We can now add this sliced row list into our `output_array`. + +```python +output_array = [[1, 2, 3, 4]] +``` + +We can then go to the **next row** index. We need to check the ending row index to make sure we do not exceed the ending row index. We can then increase our row index according to the step. Since the step size is 1, we can add this to our previous row index which was 0. + +```python +current_row_index = 0 + 1 +input_row_list = input_array[current_row_index] +``` + +We can then retrieved the input row list. + +```python +input_row_list = [5, 6, 7, 8] +``` + +We can then used the same column start index and ending index to slice the row list. + +```python +sliced_row_list = [5, 6, 7, 8] +``` + +Then, we can add this into our output array. + +```python +output_array = [[1, 2, 3, 4], + [5, 6, 7, 8]] +``` + +We can imagine how it continues to the end until our row index exceed the endign row index given in the input. + +Now if the starting row index is not 0, we just need to start our current index from that non-zero index. Similary, if the step size is not 1, we just need to add our step size accordingly. + +```python +current_row_index = row_start +``` + +```python +current_row_index = current_row_index + row_step +``` + +We just need to continue going through the rows until the current row index is equal or greater than the ending row index. + +Getting the elements in a row list is easier. Since each row is a list, we can use normal slicing to get the sliced row list. + +``` +sliced_row_list = input_row_list[col_start: col_end: col_step] +``` + +Let's generalize these steps and write down in words. + +### (D)esign of Algorithm + +Recall that we first create an empty list and then go through the rows. + +``` +1. Create an empty array for the output. +2. For each row in the input row from row_start to row end with row_step size, do: + 2. 1 ... +``` + +What are the things that we repeat at each row? We first get the row list and sliced it using the column indices. After we slice it, we must not forget to add the sliced list into our output array. We then repeat these steps for the next row list. + +``` +1. Create an empty array for the output. +2. For each row in the input row from row_start to row end with row_step size, do: + 2.1 get the current row list from the input array. + 2.2 slice the current row list using the column start and end indices with its step size. + 2.3 add the sliced row list into the output row. +``` + +Let's start the Implementation and Testing. + +### (I)mplementation and (T)est + +Let's start by defining our function with the respective input and output. + +```python +from typing import Any + +def slice_2d(array: list[list[Any]], + row_start: int, row_end: int, row_step: int, + col_start: int, col_end: int, col_step: int) -> list[list[Any]]: + pass + +input_array: list[list[int]] = [[1, 2, 3, 4], + [5, 6, 7, 8], + [9, 10, 11, 12], + [13, 14, 15, 16]] + +output: list[list[int]] = slice_2d(input_array, 0, 4, 2, 0, 4, 2) +print(output) +``` + +To test our function, we need to call it which is what the last line does. Notice that we have annotated the input and output of the function accordingly following our problem statement. Running `mypy` on the above code produces no error. We created `slice_2d.py` file inside `lesson08` folder for you to try. + +``` +$ mypy slice_2d.py +Success: no issues found in 1 source file +``` + +We are now ready to implement our algorithm. The first step in our design of algorithm is to create an empty output array. For brevity sake, we will not include the function call and only show the function definition in the subsequent codes. + +```python +from typing import Any + +def slice_2d(array: list[list[Any]], + row_start: int, row_end: int, row_step: int, + col_start: int, col_end: int, col_step: int) -> list[list[Any]]: + + output: list[list[Any]] = [] + # your solution + return output +``` + +If we run this code, it will output an empty list. So we can move on to step number 2. We should iterate the rows from the starting index for the rows all the way to the ending index with step size as indicated by the input. We can use `range()` function to create our `current_row_index`. Let's create this row index and print it out. + +```python +from typing import Any + +def slice_2d(array: list[list[Any]], + row_start: int, row_end: int, row_step: int, + col_start: int, col_end: int, col_step: int) -> list[list[Any]]: + + output: list[list[Any]] = [] + for current_row_index: int in range(row_start, row_end, row_step): + print(current_row_index) + return output +``` + +Given the following function call `slice_2d(0, 4, 1, 0, 4, 1)`, it gives the following output. + +``` +0 +1 +2 +3 +[] +``` + +We have set the row indices to start from 0 to 4 (exclusive) with step size 1. We should test what happens if we change the function call to the following. + +```python +output:list[list[int]] = slice_2d(input_array, 0, 4, 2, 0, 4, 2) +print(output) +``` + +In this case, we use step size 2 for both the rows and the columns. The output is given below. + +``` +0 +2 +[] +``` + +So far looks good. We can get the indices of our sliced rows. Now, we can proceed to do step 2.1 which is to get the list of the current row. + +```python +from typing import Any + +def slice_2d(array: list[list[Any]], + row_start: int, row_end: int, row_step: int, + col_start: int, col_end: int, col_step: int) -> list[list[Any]]: + + output: list[list[Any]] = [] + for current_row_index: int in range(row_start, row_end, row_step): + row_list: list[Any] = array[current_row_index] + print(row_list) + return output +``` + +The output for the two function calls above is shown below. + +``` +[1, 2, 3, 4] +[5, 6, 7, 8] +[9, 10, 11, 12] +[13, 14, 15, 16] +[] +[1, 2, 3, 4] +[9, 10, 11, 12] +[] +``` + +In the first function call, we have four rows (from 0 to 3) and we can see the list for each row. There is one line with the output `[]` which is the result of printing the `output` list at the end of each function call. The second function call has a step size of two and it gets row 0 and row 2 which gives us `[1, 2, 3, 4]` and `[9, 10, 11, 12]` respectively. + +Once we get the row list, we can slice to get the elements according to our column indices. + +```python +from typing import Any + +def slice_2d(array: list[list[Any]], + row_start: int, row_end: int, row_step: int, + col_start: int, col_end: int, col_step: int) -> list[list[Any]]: + + output: list[list[Any]] = [] + for current_row_index: int in range(row_start, row_end, row_step): + row_list: list[Any] = array[current_row_index] + sliced_row_list: list[Any] = row_list[col_start: col_end: col_step] + print(sliced_row_list) + return output +``` + +The output for the two function calls above is shown below. + +``` +[1, 2, 3, 4] +[5, 6, 7, 8] +[9, 10, 11, 12] +[13, 14, 15, 16] +[] +[1, 3] +[9, 11] +[] +``` + +Notice that for the first function call, we get all the elements in the column. This is because we set the step size to be one. On the other hand, the second function calls output `[1, 3]` and `[9, 11]`. The reason is that we set the column step size to be two. + +The last step is to add this sliced list into our output list. + +```python +from typing import Any + +def slice_2d(array: list[list[Any]], + row_start: int, row_end: int, row_step: int, + col_start: int, col_end: int, col_step: int) -> list[list[Any]]: + + output: list[list[Any]] = [] + for current_row_index: int in range(row_start, row_end, row_step): + row_list: list[Any] = array[current_row_index] + sliced_row_list: list[Any] = row_list[col_start: col_end: col_step] + output.append(sliced_row_list) + return output +``` + +The output is given as shown below for the two function calls. + +``` +[[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12], [13, 14, 15, 16]] +[[1, 3], [9, 11]] +``` + +We can now apply this function to our `month_steps` list of list. Let's say, we want to get the steps on Wednesday for each week. We can write the following code. + +```python +month_steps: list[list[int]] = [[40, 50, 43, 55, 67, 56, 60], + [54, 56, 47, 62, 61, 46, 61], + [52, 56, 63, 58, 62, 66, 62], + [57, 58, 46, 71, 63, 76, 63]] +wed_steps: list[list[int]] = slice_2d(month_steps, 0, 4, 1, 3, 4, 1) +print(wed_steps) +``` + +Notice that we want all weeks so we set our row start index to be zero and its ending to be four with step size one. However, since we only wants Wednesdays in each week, we set our column start index to three and its ending index to four (exclusive) with a step size of one. The output is shown below. + +``` +[[55], [62], [58], [71]] +``` + +We can also get the steps on every Wednesdays and Fridays in the first two weeks using the following code. + +```python +wed_fri_week12: list[list[int]] = slice_2d(month_steps, 0, 2, 1, 3, 6, 2) +print(wed_fri_week12) +``` + +Verify that the input arguments are what you expected. The output is shown below. + +``` +[[55, 56], [62, 46]] +``` \ No newline at end of file diff --git a/_Notes/Object_Oriented_Programming.md b/_Notes/Object_Oriented_Programming.md deleted file mode 100644 index f0a0bae..0000000 --- a/_Notes/Object_Oriented_Programming.md +++ /dev/null @@ -1,662 +0,0 @@ ---- -title: Object Oriented Programming -permalink: /notes/oo_programming -key: notes-oo-programming -layout: article -nav_key: Notes -sidebar: - nav: Notes -license: false -aside: - toc: true -show_edit_on_github: false -show_date: false ---- - -## What is Object Oriented Programming? - -[Object-oriented programming](https://en.wikipedia.org/wiki/Object-oriented_programming) (OOP) is a programming paradigm based on the concept of "objects". - -As your program grows in complexity, you may need something more than simple built-in data types such as `str`, `int`, or `list`. For example, when you create a game, you may need an `Avatar`, or `Weapon`, etc. In these cases, it is easier to organize your code around objects. You can think of objects as your own user-defined data types. Later you will see that these objects have two main things: - -- attributes: which defines the characteristic of the object, and -- methods: which defines what the object can do - Attributes and methods define your object. - -You actually have worked with objects if you use `list` and `str` data type in your program. These are called **built-in** objects in Python. Python has provided these objects for you to use. What we will do in this section is to create your own **user-defined** objects. - -We will see that user-defined objects are made of other data (attributes) and computations (methods). Moreover, we will see that **any** code can be abstracted as an **object** since any computer code are made of data (attributes) and some computations (methods). In these lessons, we will see how OOP will be used for both creating user-defined data type as well as for abstracting the whole program. - -## Attributes and Methods - -For example, let's say you want to create a computer game with a Robot Turtle as its character. In this case, you may want to define a new data type called `RobotTurtle`. `RobotTurtle` will have the **attributes** `speed` and `name`. Attributes describes the object and its properties. It is usually a _noun_ and it is defined as a kind of variable within the object. On the other hand a Robot Turtle can `move`. So the data type `RobotTurtle` would have a **method** called `move`. Methods are a kind of functions which apply to our user-defined data type. A method describes what the object can do and so it is usually a _verb_. In order to create our user-defined objects, we have to do the following: - -1. Define a class, which defines the object with its attributes and methods -1. Instantiate an object, which actually creates the object - -The class definition tells Python about your user-defined object and how to create it. It tells Python what attributes this object has using some existing built-in data types or other defined objects. It tells Python what methods the object can do. But it is important to note that a class definition is just like a kind of contract on a piece of paper. The contract does not create the object. _Instantiation_ is the step that actually creates the object in the computer's memory. We will show these two steps below. - -First, let's start by defining our `RobotTurtle` class. - -```python -# Class definition -class RobotTurtle: - # Attributes: - def __init__(self, name, speed=1): - self._name = name - self._speed = speed - self._pos = (0, 0) - - # Methods: - def move(self, direction): - update = {'up' : (self._pos[0], self._pos[1] + self._speed), - 'down' : (self._pos[0], self._pos[1] - self._speed), - 'left' : (self._pos[0] - self._speed, self._pos[1]), - 'right' : (self._pos[0] + self._speed, self._pos[1])} - self._pos = update[direction] - - def tell_name(self): - print(f"My name is {self._name}") -``` - -Some notes on the class definition: - -- Notice the above code starts with a Class Definition. To define a class, we use the keyword `class` followed by the class name `RobotTurtle`. -- The keyword `def` inside the class defines the **method** which tells Python what the object can do. -- The first method is special and it is called `__init__()`. This method is always called during _object instantiation_. This special method is called to _initialize_ the object's attributes during _instantiation_. In this definition, we see that during instantation, we ask Python to initialize three attributes: - - `_name` which is a string and is initialized using the first argument during object instantiation. - - `_speed` which is a number and is initialized using the second optional argument. - - `_pos` which is the position using a tuple of two numbers and is initalized to `(0,0)`. -- The class definition also contains two other **user-defined** methods: - - `move(direction)` which is to move the Robot Turtle to certain direction according to its speed. - - `tell_name()` which is to print out the name of the Robot Turtle. - -It is important to remember that the class definition is just a description of the object and works as a kind of template or contract. The definition does not create the object itself. The object creation happens by doing the following: - -```python -# Object Instantiation -my_robot = RobotTurtle("T1") -``` - -The above line is what we call as **object instantiation**. When Python executes this lines, it _instantiates_ an object of the type `RobotTurtle` in the memory. A few notes on object instantation: - -- The object is created or _instantiated_ by using the class name followed by some values used to initialze the object. In this case: `RobotTurtle("T1")`. The argument "T1" is passed on to initialize the object's name. This object is then pointed to by the variable `my_robot`. -- Each of the argument in the _object instantiation_ is passed on to the `__init__()` method. In this case, "T1" is passed on to the formal argument `name` in `__init__()`. -- The **first** argument of any method in a class is always called `self` following Python's [PEP8](https://www.python.org/dev/peps/pep-0008/). The `self` argument is also found as the first argument inside the method `move` and `tell_name`. The first argument `self` refers to the particular object instance of the class. It can also be used to access methods and attributes of the current object. -- At the end of object instantiation, the object `my_robot` would have the following attributes initalized: - - `_name` with a value `T1` - - `_speed` with a value of `1` - - `_pos` with a value of `(0,0)` - -Once the object is created, we can access its attributes and methods. For example, you can ask the robot to tell its name. - -```python -# Accessing object's method -my_robot.tell_name() -``` - -The output is - -```sh -My name is T1 -``` - -- `my_robot.tell_name()` is calling the method `tell_name()` using the **dot operator**. To call any method, we use the format of - ```python - object.method_name(arguments) - ``` -- If you run the cell above, you will see "My name is T1" printed on the output - -You can actually access the attributes directly and change it, for example - -```python -# accessing object's attribute -print(my_robot._speed) -my_robot._speed = 2 -print(my_robot._speed) -``` - -The output is - -```sh -1 -2 -``` - -- the first and the third line access the object's attribute using the **dot operator**. -- the second line assigned the value 2 into the object's `_speed` attribute. -- if you run the cell above, you will see the speed changes from 1 to 2. - -The following examples show more examples on how one can access object's attributes and methods using the dot operator. - -```python -my_robot = RobotTurtle("T2", 2) - -print(f'Robot {my_robot._name} initially at {my_robot._pos}') -for _ in range(4): - my_robot.move('up') - print(f'Robot {my_robot._name} now at {my_robot._pos}') - my_robot.move('right') - print(f'Robot {my_robot._name} now at {my_robot._pos}') -``` - -The output is - -```sh -Robot T2 initially at (0, 0) -Robot T2 now at (0, 2) -Robot T2 now at (2, 2) -Robot T2 now at (2, 4) -Robot T2 now at (4, 4) -Robot T2 now at (4, 6) -Robot T2 now at (6, 6) -Robot T2 now at (6, 8) -Robot T2 now at (8, 8) -``` - -Note: - -- We create a new object with the name "T2" and speed of 2. -- We first printed its initial position by access `my_robot._pos` attribute. -- Then, we iterate four times. Since we don't use of the iteration variable, we make us of `_`. -- In the iteration, we move up and then move right. After each movement, we printed the position. - -## Encapsulation and Properties - -One important concept of Object Oriented programming is called **Encapsulation**. The idea of encapsulation is that data should be bundled together with some methods to access it. The data itself should be hidden from those outside of the object. With encapsulation, the state of the object is hidden from those outside of the object. If anyone would like to change the state of the object or enquire about the state of the object, it has to do so using some **methods**. - -Why would we want to have this encapsulation? One of the purpose is to make the object transparent. Anyone working with the object does not need to know how the state or the data inside the object is implemented. For example, we implement the position attribute in our Robot Turtle object as a tuple of two numbers. This assumes those assigning value to this position always assign a tuple with two numbers. What if they don't? Let's illustrate this with an example - -If we let others access the attributes direclty, one can assign non number data into the position attribute, such as the following example. - -```python -my_robot._pos = "This is not supposed to be allowed" -print(my_robot._pos) -``` - -The output is - -```sh -This is not supposed to be allowed -``` - -Such assignment should not be allowed in the first place. If it is allowed, then our `move()` method will produce an error now as shown by running the following cell. - -```python -my_robot.move("up") -``` - -The output is - -```sh ---------------------------------------------------------------------------- -TypeError Traceback (most recent call last) -/var/folders/9l/s5tr888d1yldwlfg3_yyk7380000gq/T/ipykernel_18003/3587695430.py in -----> 1 my_robot.move("up") - -/var/folders/9l/s5tr888d1yldwlfg3_yyk7380000gq/T/ipykernel_18003/2532245999.py in move(self, direction) - 9 # Methods: - 10 def move(self, direction): ----> 11 update = {'up' : (self._pos[0], self._pos[1] + self._speed), - 12 'down' : (self._pos[0], self._pos[1] - self._speed), - 13 'left' : (self._pos[0] - self._speed, self._pos[1]), - -TypeError: can only concatenate str (not "int") to str -``` - -Therefore, it is important that we do encapsulation. Encapsulation ensures that any access to the data should be done through some specific methods. There are two kinds of methods for this purpose: - -- enquiry or _getter_: this method is used to get or enquire the state of the object -- modifier or _setter_: this method is used to modify or set the state of the object. - -In Python, we do this using the concept of **property**. A _property_ represents an attribute with its getter and setter. Let's rewrite the class using property this time. We are going to create two properties, one for `name` and the other one for `speed`. On the other hand, we wil create a property for position only with a getter. The reason is that we want position to be modified only by calling the `move()` method. - -```python -# Class definition -class RobotTurtle: - # Attributes: - def __init__(self, name, speed=1): - self.name = name - self.speed = speed - self._pos = (0, 0) - - # property getter - @property - def name(self): - return self._name - - # property setter - @name.setter - def name(self, value): - if isinstance(value, str) and value != "": - self._name = value - - # property getter - @property - def speed(self): - return self._speed - - # property setter - @speed.setter - def speed(self, value): - if isinstance(value, int) and value > 0: - self._speed = value - - # property getter - @property - def pos(self): - return self._pos - - # Methods: - def move(self, direction): - update = {'up' : (self.pos[0], self.pos[1] + self.speed), - 'down' : (self.pos[0], self.pos[1] - self.speed), - 'left' : (self.pos[0] - self.speed, self.pos[1]), - 'right' : (self.pos[0] + self.speed, self.pos[1])} - self._pos = update[direction] - - - def tell_name(self): - print(f"My name is {self.name}") -``` - -We define a property for `name` as follows: - -```python - # property getter - @property - def name(self): - return self._name - - # property setter - @name.setter - def name(self, value): - if isinstance(value, str) and value != "": - self._name = value -``` - -Note: - -- We use the syntax `@property` to define a getter with the name `name`. This is what is called as **decorator** in Python. A decorator allows you to modify the function defined in the line just after it. In our case, it changes the method `def name(self)` into a **getter** method for a property called `name`. -- The setter is defined using a decoratory `@name.setter`. In this setter method, we ensure that only those of the type `str` and not empty string can be assigned to the attribute `_name`. -- This setter will be called in the `__init__()` since the argument is assigned to the **property** `name` and not to the **attribute** `_name`, i.e. `self.name = name`. - -The property for the `speed` is defined similarly. - -```python - # property getter - @property - def speed(self): - return self._speed - - # property setter - @speed.setter - def speed(self, value): - if isinstance(value, int) and value > 0: - self._speed = value -``` - -Note: - -- The setter decorator is `@speed.setter` where the name before the dot is the name of the _property_. -- The setter ensures that only integer greater than 0 can be assigned to the attribute `_speed`. - -Let's see some examples on how to use the properties. - -```python -# this is to create a new object with property, make sure you run the cell with the class definition first -my_robot = RobotTurtle("T4") - -# enquire name and speed -print(my_robot.name) -print(my_robot.speed) -``` - -The output is - -```sh -T4 -1 -``` - -Notice that you use the property name, which are `name` and `speed` respectively instead of its attributes name, i.e. `__name` and `__speed`. This access calls the **getter** method of the respective properties. - -Moreover, you can also change the value using the assignment operator which will call the **setter** method. - -```python -my_robot.name = "T4new" -print(my_robot.name) -my_robot.name = "" -print(my_robot.name) -``` - -The output is - -```sh -T4new -T4new -``` - -Notice that in the second assignment, the name is not assigned to an empty string. It remains as `T4new`. The reason is that our setter only assigns the value if the value is a string and non-empty. Similarly, we can see the same behaviour for speed property. - -```python -my_robot.speed = 2 -print(my_robot.speed) -my_robot.speed = -2 -print(my_robot.speed) -``` - -The output is - -```sh -2 -2 -``` - -Notice that the second assignment to -2 did not go through because of our setter method's checking. - -On the other hand, we do not have any setter for position. The reason is that we want position to always start from `(0, 0)` and it can only change its position through the method `move()`. Note, however, that we are using a **single leading underscore** as a convention for people not to touch it. We can still enquire the position using the property's getter. - -```python -print(my_robot.pos) -``` - -The output is - -```sh -(0, 0) -``` - -To change its position, it should call the `move()` method. - -```python -my_robot.move("up") -my_robot.move("up") -print(my_robot.pos) -``` - -Note that we use the **properties**'s names `self.pos` and `self.speed` in updating the attribute `_pos` and `_speed`. See the `move()` method. - -```python - def move(self, direction): - update = {'up' : (self.pos[0], self.pos[1] + self.speed), - 'down' : (self.pos[0], self.pos[1] - self.speed), - 'left' : (self.pos[0] - self.speed, self.pos[1]), - 'right' : (self.pos[0] + self.speed, self.pos[1])} - self._pos = update[direction] -``` - -You can actually still access the attributes since Python does not have a concept of private attribute. This is how you access the attributes with a double leading underscore in its name. - -```python -my_robot._pos -``` - -The output is - -```sh -(0, 0) -``` - -But it is a convention in Python that when you use a single leading underscore, people should not touch it directly. On the other hand, one can also use a **double leading underscores**. This allows [Name Mangling](https://stackoverflow.com/questions/7456807/python-name-mangling) that prevents accidental overloading of methods and name conflicts when you extend a class. - -In summary on the use of leading underscore for attribute's name: - -- When in doubt, leave it "public". This means that we should not add anything to obscure the name of your class' attribute. -- If you really want to send the message "Can't touch this!" to your users, the usual way is to precede the variable with one underscore. This is just a convention, but people understand it and take double care when dealing with such stuff. -- The double underscore magic is used mainly to avoid accidental overloading of methods and name conflicts with superclasses' attributes. It can be quite useful if you write a class that is expected to be extended many times. We will talk about inheritance to extend a class in the subsequent lessons. - -The above summary are taken from [this article](https://stackoverflow.com/questions/7456807/python-name-mangling). - -## Computed Property - -Both `name` and `speed` are what is commonly called **stored** properties. For each stored property there is a corresponding attribute. We can also create what is called **computed** property. A computed property retrieves its value from some other attributes and does not have a setter. To illustrate, let's create a new user-defined object called `Coordinate`. - -```python -import math - -class Coordinate: - - def __init__(self, x=0, y=0): - self.x = x - self.y = y - - @property - def distance(self): - return math.sqrt(self.x * self.x + self.y * self.y) -``` - -In the above class, we have two attributes `x` and `y`. We do not create any properties for these attributes for simplicity. Python encourages simplicity anway. But here, we create a computed property called `distance`. This property returns the distance of the current x and y from its origin (0, 0). We can test by instantiating the object and assign some values to its attributes. - -```python -# object instantiation -p1 = Coordinate(3, 4) -print(p1.x, p1.y) -print(p1.distance) -``` - -The output is - -```sh -3 4 -5.0 -``` - -The last line prints the computed property `distance` which is computed from the two attributes `x` and `y`. Notice here that `distance` is printed without parenthesis and so it is not a **method** but rather a **property**. - -So we may ask when should we use a method that returns a value and when to use a computed property. Here are some considerations: - -- A method can have arguments. This means that if your returned value requires some input other than the attributes of its object, you must use a method rather than a computed property. -- A method describes an action. If the code performs some actions and return the output of that action, then a method is more suitable. - -So when should we use a computed property? - -- When the property describes some intrinsic quality of the object. Property is similar in many ways to attribute and it is usually a "noun". It should describes some kind of property of the object rather than some action that the object can do. -- When the computation is simple and cheap. We should prefer property for simple values you can get by doing a quick calculation. Distance property in the example above is a good example of this. -- When you can compute the value only with the object's attributes. Remember that getter of a property does not take any other argument besides `self`. This means that the computed value must be obtained only from the object's attributes. - -## Composition - -An object can be composed of other objects. For example, we have seen that our `RobotTurtle` object is made up of other objects such as `str` for its name, `int` for its speed and tuple for its position. We can also compose an object from other **user-defined** object. For example, instead of using a tuple for its position, our Robot Turtle class can make use of the `Coordinate` class. - -```python -# Class definition -class RobotTurtle: - # Attributes: - def __init__(self, name, speed=1): - self.name = name - self.speed = speed - self._pos = Coordinate(0, 0) - - # property getter - @property - def name(self): - return self._name - - # property setter - @name.setter - def name(self, value): - if isinstance(value, str) and value != "": - self._name = value - - # property getter - @property - def speed(self): - return self._speed - - # property setter - @speed.setter - def speed(self, value): - if isinstance(value, int) and value > 0: - self._speed = value - - # property getter - @property - def pos(self): - return self._pos - - # Methods: - def move(self, direction): - update = {'up' : Coordinate(self.pos.x, self.pos.y + self.speed), - 'down' : Coordinate(self.pos.x, self.pos.y - self.speed), - 'left' : Coordinate(self.pos.x - self.speed, self.pos.y), - 'right' : Coordinate(self.pos.x + self.speed, self.pos.y)} - self._pos = update[direction] - - - def tell_name(self): - print(f"My name is {self.name}") -``` - -We made two main changes. First, in the `__init__()` instead of initializing to a tuple, we instantiate an object `Coordinate()`. - -```python - def __init__(self, name, speed=1): - self.name = name - self.speed = speed - self._pos = Coordinate(0, 0) -``` - -The initial position is still at (0, 0) but now the type is no longer a tuple, but rather, a `Coordinate` class. The second change is on the `move()` method. - -```python - def move(self, direction): - update = {'up' : Coordinate(self.pos.x, self.pos.y + self.speed), - 'down' : Coordinate(self.pos.x, self.pos.y - self.speed), - 'left' : Coordinate(self.pos.x - self.speed, self.pos.y), - 'right' : Coordinate(self.pos.x + self.speed, self.pos.y)} - self._pos = update[direction] -``` - -Instead of using indices like `self.pos[0]` and `self.pos[1]`, we now use the dot operator with its attribute names like `self.pos.x` and `self.pos.y`. This is much a clearer and easy to read as compared to using indices. Moreover, instead of using a tuple, we instantiate `Coordinate()` object as the value of the dictionary `update`. - -We can now create the object and test our new class as follows. - -```python -my_robot = RobotTurtle("T with Coordinate") -print(my_robot.pos) -``` - -The output is - -```sh -<__main__.Coordinate object at 0x7fa838655760> -``` - -Notice that now `pos` is a `Coordinate` object. We can access its attributes as usual. - -```python -print(my_robot.pos.x, my_robot.pos.y) -``` - -The output is - -```sh -0 0 -``` - -We can move the robot using the `move()` method. - -```python -my_robot.move("right") -my_robot.move("down") -print(my_robot.pos.x, my_robot.pos.y) -``` - -The output is - -```sh -1 -1 -``` - -## Special Methods - -Some methods' name in Python are special and can be overridden. One example of special method that you have encountered is `__init__()` method. This method is always called during object instantiation. There are many other special methods, but for now, we will introduce one more, which is the `__str__()` method. This method is called when Python tries to convert the object to an `str` object. One common instance of this is when you print the object. - -If we print the `Coordinate()` object, we will see the following output. - -```python -p1 = Coordinate(2, 3) -print(p1) -``` - -The output is - -```sh -<__main__.Coordinate object at 0x7fa838655b80> -``` - -Python basically does not understand how to print a `Coordinate()`. But we can tell Python how to convert this object into an `str` which Python can display into the screen. Let's override the method `__str__()`. - -```python -import math - -class Coordinate: - - def __init__(self, x=0, y=0): - self.x = x - self.y = y - - @property - def distance(self): - return math.sqrt(self.x * self.x + self.y * self.y) - - def __str__(self): - return f"({self.x}, {self.y})" -``` - -In the above method `__str__()` we return a string whenever Python tries to convert this object into a string. Once we define this special method, we can print a `Coordinate` object. - -```python -p1 = Coordinate(2, 3) -print(p1) -``` - -The output is - -```sh -(2, 3) -``` - -Once Coordinate has this method, it can be used whenever the object has some `Coordinate` attributes. For example, we can print our robot position simply by doin gthe following. - -```python -my_robot = RobotTurtle("T with Coordinate") -print(my_robot.pos) -``` - -The output is - -```sh -(0, 0) -``` - -Recall, that previously you have to specify it as - -```python -print(my_robot.pos.x, my_robot.pos.y) -``` - -But now it is no longer necessary and Python knows how to convert your `Coordinate` object into a string which can be displayed into the standard output. - -## UML Diagram - -In designing Object Oriented programs, we usually use a [UML diagram](https://en.wikipedia.org/wiki/Class_diagram). UML stands for _Unified Modeling Language_ and it gives some specifications how to represent the classes visually. For example, our `RobotTurtle` class is drawn as the following UML class diagram. - -drawing - -The UML Class diagram consists of three compartment: - -- The first compartment on the top: this specifies the class name -- The second compartment in the middle: this lists down all the properties and attributes -- The third compartment at the bottom: this lists down all the methods - -Sometime, it is useful to identify the property's type especially when there is a case of composition as in our `pos` property. In this case, we know that `pos` is of the type `Coordinate`. This is drawn in UML diagram as follows. - -drawing - -UML diagram also allows us to specify the relationship between different classes. For example, `RobotTurtle` and `Coordinate` relationship can be drawn as shown below. - -drawing - -In this diagram, we see that one `RobotTurtle` can have one `Coordinate`. This is a specific kind of _association_ relationship called **composition**. This means that `RobotTurtle` is composed of a `Coordinate`. When the object `RobotTurtle` is destroyed, the `Coordinate` object associated with it is also destroyed. There are other kinds of relationship which we will introduce along the way. diff --git a/_Notes/PCDIT_Intro.md b/_Notes/PCDIT_Intro.md new file mode 100644 index 0000000..4b6fcf2 --- /dev/null +++ b/_Notes/PCDIT_Intro.md @@ -0,0 +1,83 @@ +--- +title: Problem Solving Framework +permalink: /notes/intro-pcdit +key: notes-intro-pcdit +layout: article +nav_key: Notes +sidebar: + nav: Notes +license: false +aside: + toc: true +show_edit_on_github: false +show_date: false +--- + +## PCDIT: How to Solve Problem Computationally? + +Many people have developed approaches for guiding novice programmers through the process of problem solving. Some Python textbooks, for example, proposed frameworks based on the steps of the software development life cycle, i.e.analysis/requirements, design, implementation, and testing. Students are encouraged to think about the input/output requirements of the problem, design a process for obtaining the output from the input, implement it, then finally test the program on a selected set of problem instances. Others have proposed a framework that expands upon the design phase by encouraging students to search for analogous problems and solutions that can be applied to the one they are tackling. + +These approaches share a typical characteristic in that testing is at the end of the process. Actually being able to get to the end, however, depends on the student's level of metacognitive awareness, i.e. their ability to think on their own about the problem. For students lacking metacognitive skills, previous research has highlighted the benefit of solving concrete cases *before* programming, as well as making the problem solving process explicit (e.g. by using an automated assessment tool) and having them reflect on their progress. Controlled experiments in these works revealed that students are more likely to provide a correctly implemented solution to a problem and demonstrate better metacognitive awareness. + + + +It is in this context that we developed PCDIT, a problem solving framework for novice programmers that encourages them to design/solve concrete cases before programming, and to regularly reflect using the five eponymous phases of the process (Figure 1{fig:pcdit_figure}): **P**roblem Definition, **C**ases, **D**esign of Algorithm, **I**mplementation, and **T**esting. A key characteristic of the framework is the **C** phase, which focuses on developing concrete cases for the problem early without actually writing any test code: students instead think about the steps at an abstract level, only mapping them down to program syntax in later phases. Another characteristic is its non-linearity: students are encouraged to engage in a reflective, case-driven, and iterative process that may feel more productive and encouraging for novices. The process keeps on going until the programming task has been solved satisfactorily, meaning that the proposed answer fulfills all the requirements described in the problem statement, and can operate correctly on all test cases. Ultimately, the scaffolding of PCDIT makes the process of problem solving that many expert programmers naturally follow, but many novices do not, explicit. + +## Five Steps in PCDIT Framework + + +The five key steps of the framework are given in Figure {fig:pcdit_figure}, and are intended to capture the problem solving process that many expert programmers naturally follow, but that novice programmers are not yet familiar with. By naming the steps and providing an acronym, we make it easier for students to reflect on where they are in the problem solving process, potentially increasing their metacognitive awareness. It is important to note that instructors and students are encouraged to go iterate between the steps as their thinking brings clarity. + +In general, the process begins with forming a **P**roblem Definition: students are asked to identify the types of inputs and outputs and summarise in natural language what it is that needs to be solved (e.g. take a single string value as input, then return the reverse of that string as output). This step is common to many problem solving frameworks in understanding the problem and formulating it. Students are encouraged to provide more detail on the kind of data involved in both the input and the output and how it can be represented in the program. This step also requires students to summarise the problem in a single statement. + +The second phase asks students to develop concrete **C**ases, i.e. before even thinking about the algorithm. The intention is for students to conceptualise the abstract steps from concrete inputs to outputs, helping them to generalise to an algorithm more easily in later parts of the framework. The **C**ases step is similar to the functional examples in "How to Design Programs"[^1]. As students work on various concrete cases, they can also step back and revise their problem definitions, e.g. adding additional information about the required data types. This step is crucial for novice programmers, many of whom do not have any existing algorithmic patterns or schemas: it may be difficult for such students to search for analogous problems. They need to build the solution from the bottom up and this concrete **C**ases step provides a bridge to figure out the algorithmic solution in the next step. Working out specific concrete cases helps students to understand the problem better, and there is evidence it helps them in implementing their solutions. We highlight that while some frameworks encourage students to write cases as part of their testing code, in PCDIT, they focus on *working out the abstract steps* (e.g. on paper) from concrete inputs to outputs. + +[^1]: https://htdp.org + +[Click here to view a PCDIT worksheet.](https://sutdapac-my.sharepoint.com/:b:/g/personal/oka_kurniawan_sutd_edu_sg/EcndMQ0kX_pBsEXHoaH2VXsBaNH1xFfrMYNRgl4AroWubQ?e=lduRSt) + +Once students have worked on these concrete cases, they can begin the **D**esign of Algorithm phase: for each concrete input/output, we ask them to enumerate the steps they did in working out the concrete **C**ases. They are asked to look back on how they arrive at the output starting from the input. We then ask them to identify patterns in those steps and generalise them to computational steps. These steps can be written in a mix of pseudo-code and (precise) natural language---whatever the student is currently more comfortable with. This part can be iterated several times, starting with more coarse subgoals/descriptions, before refining them over the iterations, e.g. by employing specific *key words/phrases*, such as `for every element in ...`, `as long as ...`, or `compare if ...`. Using specific keywords that sound similar to programming language syntax eases the transition from pseudo-code to actual code later. Figures {fig:annotated_worksheet}--\ref{fig:annotated_worksheet_DI} illustrate how one can take some concrete **C**ases for a problem and start to sketch an algorithm **D**esign in an intuitive (but not yet fully refined) way. + +In the subsequent steps, students start to map the pseudo-code of their solution down to concrete Python in the **I**mplementation phase. In our teaching, we iterate this part of the framework with the **T**esting phase, ensuring that students are regularly testing their programs after completing every few lines of mapping. This helps to ensure novices feel motivated and productive by tackling smaller/feasible sub-problems one-by-one. In testing their code, students can use some of the **C**ases identified earlier, or propose new ones that potentially highlight the need to go back and improve other aspects of the algorithm further. The **I**mplementation step in Figure \ref{fig:annotated_worksheet_DI} actually contains a syntax error in the list operation, illustrating the importance of going back and revising the initial implementation after the **T**esting step. + +Throughout this notes, we will emphasize the various steps of PCDIT and encourage students to work out the these steps as well when they encounter a new programming problems. + +## Novice and Expert Programmers + +It is generally thought that it takes about 10 years to turn a novice into an expert programmer. It is important to highlight how expert programmers think and solve problems so that novice programmers can move towards that. In this section, we will highlight the difference between novice and expert programmers. + +### Knowledge Schemas + +Expert programmers efficiently organized knowledge schemas according to functional characteristics of the underlying algorithms. On the other hand, novice programmers tend to be limited to surface and superficial knowledge such as the language syntax. An example of this is that when expert programmers see a problem description, they tend to think how similar this new problem with existing problems they have solved before. Expert programmers have mental bank of large schemas of various problems and their underlying algorithms to solve these problems. On the other hand, novice programmes have not built up their solution schemas and so limited to thinking in terms of the programming language syntax such as what statement should they use in this problem. + +This indicates that novice programmers need to learn to build their solution schemas by exposing themselves to various problems and their solutions. At the same time, they need to learn to identify patterns in those problems they see. Novice programmers also need to learn to use their existing solution schemas to solve the new problems instead of focusing on the language syntax. + +### Strategies + +Expert programmers tend to use both general problem solving strategies such as divide-and-conquer as well as specialised strategies for particular problems that they face. They are flexible enough to choose the strategies to employ. This owes to their existing knowledge schemas. In this way, they tend to do a top-down, breadth-first approach to decompose and understand programs. + +On the other hand, novice programmers tend to approach programming "line by line" rather than using meaningful program "chunks" or structures. Moreover, they may have shortcomings in their planning strategies on how to reach the goal. + +This leads to similar conclusion as the previous one. Novice programmers need to focus on expanding their knowledge schemas and focus on the strategies instead of just the programming language itself. However, programming language is important and novice programmers must strive to master the various programming language constructs. + +### Language Constructs + +Studies show that expert programmers are familiar and comfortable to use specific programming language constructs to solve the programming problems. On the other hand, novice programmers struggles with the programming language constructs. They tend to use a few common ways and syntax of the language. Novice programmers also tend to focus on the programming language constructs instead of the algorithm. They tend to think what syntax should I use instead of what kind of steps should I do to solve this problem. + +One way to help novice programmers is to strengthen their familiarity with the programming language constructrs that they use. It is important to give the understanding how the programming language works and to expose them how different constructs are used in solving different problems. Novice programmers also need to write code on their own instead of copy pasting a code. Writing the codes using these different constructs helps to make them familiar with the programming language itself. + +Many times, programming language learning is linear from one topic to another topic. However, the way our memory works require us to revisit some of these topics, exercise again and again to make one comfortable with the language. Just as learning a new foreign language requires someone to make use of the language frequently, it would be the same with learning a new programming language. + +At the same time, learning programming language alone without any concrete problems to solve may rob the context from the learner and results in a reduced motivation. The challenge for educator is to introduce the language construct which at the same time motivates novice programmers to use them and to use them repeatedly to the point that they are comfortable and familiar with the language. + +### Testing + +Expert programmers test their codes and many even write the test before they write their solutions. Novice programmers tend to put testing outside of their scope of learning programming. In most courses, test code are given for novice programmers. Moreover, they are not taught on how to test their code frequently in small chunks. Novice programmers are not aware when to test their code or how to test their code. + +Basic testing and debugging skills must be introduced to novice programmers from the early times. Certain concepts of programming language such as basic code invariant are useful to help them in testing their code. + +## Summary + +This section introduce the readers on the problem solving framework which we adopt in this notes. We also share the difference between the expert and novice programmers in approaching a problem. Our hope is that by identifying some these differences, we can lead any novice programmers towards the direction that expert progammers have. We will highlight some of the steps in the PCDIT framework. Moreover, we will also put some highlight on how expert and novie programmers may think in approaching a particular problem examples. + +We also need to be aware that these various skills needed may result in cognitive overload for novice programmers. It is the task of educators then to scaffold learners to pick up all these various skills. \ No newline at end of file diff --git a/_Notes/Programming_Intro.md b/_Notes/Programming_Intro.md new file mode 100644 index 0000000..a732ee4 --- /dev/null +++ b/_Notes/Programming_Intro.md @@ -0,0 +1,80 @@ +--- +title: Computational Thinking and Programming +permalink: /notes/intro-programming +key: notes-intro-programming +layout: article +nav_key: Notes +sidebar: + nav: Notes +license: false +aside: + toc: true +show_edit_on_github: false +show_date: false +--- + +## What is Programming? + +We have previously talked on what is computational thinking and how it is related to programming. But what is programming and how it may help our computational thinking skills. + +When you use computers for your work or study, you will naturally use some softwares or applications. These softwares or applications are also called *programs*. The activity that creates these *programs* or softwares is called *programming*. The result of a programming activity is what we call as *computer codes*. Computer codes are a series of instruction that can be run on the computer to do some particular tasks. + +For example, a web browser software is created by writing computer codes that takes in a web address from the address bar, makes a request to the internet and displays the website in the browser. Every web browser uses a web browser engine that interpret the returned response from a website to the browser. One example of a web browser engine is WebKit and you can see its *codes* online[^1]. + +[^1]: https://github.com/WebKit/WebKit + +To create these computer codes, one usually choose a *programming language* to work with. Every computer code is written in some specific programming language. How then computer processes these codes? There are two main ways: using an interpreter or a compiler. + +In using an interpreter, the computer processes every *statement* or *expression* written in the codes and executes it immediately. For example, let's take a look at a Python code below to calculate a Body Mass Index. + +```python +weight = 60 +height = 170 +bmi = weight / (height * height) +print(bmi) +``` + +The above Python *code* has four lines. Python is an interpreted programming language which means that every statement will be evaluated and executed before the next one. In the code above, a Python interpretter will read the first line, i.e. `weight = 60` and do something. What does Python interpreter do? We will go into more details in future lessons but for now it is sufficient to say that Python creates an integer data and assign it to variable called `weight` in its environment. Python interpreter does this before it executes the next line, i.e. `height = 170`. + + + +The second way is in using some *compiled* programming languages such as C and C++. These programming languages have *compilers* that translates the computer codes into some object files which contain the machine level instructions to the CPU (computer processing unit). Most of the time a *linker* is used to link various object files to a *binary* files that is executable by the computer. A similar code written in C is shown below. + +```c +int main(void){ + int weight = 60; + int height = 160; + float bmi = weight / (height * height); + printf(bmi); +} +``` + +Unlike interpreted language, where the instructions are executed line by line, the whole source code is *compiled* into an object file which is then linked into an executable file. Only then the computer executes the program. + + + +Since compilers take in the whole source code, they are able to do optimisation and checking on the codes. Optimisations improve the performance of the codes while checking may reduce errors before the codes is being executed. For example, variables that are not used nor initialized may produces errors or warnings during compilation time. Moreover, loops can be optimised and memory allocation can be saved by compilers too. Interpreters on the other hand do not have these advantages since the codes are executed one statement at a time. Some interpreted programming languages try to create a Just-in-time compilers to increase the performance. + + +The important things to note is that the result of a programming activity is a source code which is used to produce instructions for the computer to execute. + + +## Skills Needed in Programming + +Programming, however, is not an easy task. This is especially true for novice programmers. The reason for this is that programming requires a number of different skill sets. Here we list down four of them which is common in most programming activities. +- Familiarity with the programming language +- Problem solving skills +- Familiarity with the development environment tools +- Analytical thinking and debugging skills + + + +Each of this different skill is required for programmers to perform a single task of programming. Familiarity with a programming language used is analogous to learning a new foreign language. At the same time, knowing programmng language does not guarantee a person to be able to write a computer code. The reason is that the programming language itself is just a tool to solve some problems at hand. Therefore, problem solving skill is also needed in programming. It is common for novice programmers to learn the programming language without being able to write the code to solve certain problems. + +Moreover, programmers usually use certain development tools such as Integrated Development Environment (IDE) or other softwares to write the computer codes, debug, test and executes them. This means that a novice programmers must learn how to use these tools. In some occasions, the installation and operation of these software tools are not easy. This means that novice programmers must juggle to learn how to use the softwares on top of the new programming language and the problem solving skills they need to acquire. One simple example is that many novice programmers do not know how to use the debugging features of their IDE. This makes it difficult for them to fix their code when they encounter some errors. + +Speaking about fixing codes, this is another skill that is different from problem solving and learning a new language. There are two kinds of errors that programmers may encounter. The first one is a syntax error where the compiler or the interpreter is unable to execute the command that the programmer writes due to some mistakes in the way the code is written. Usually the compiler or the interpreter is able to highlight what kind of syntax error occurrs and which line of the code that it fails. Unfortunately, many novice programmers are not able to understand the error message that the compiler or the interpreter gives. Some of these error messages produce a whole stack of information that is useful for expert programmers but may confuse a novice programmer. + +The second kind of error is what is called semantic errors or logical errors. These errors are not caught by the compiler or the interpreter because the codes are written in a valid syntax or instruction for the machine to execute. However, these are called semantic or logical errors because they produce a different output than what is expected. Being able to trace where the logical errors are requires some analytical skills. + +The various skills that is needed by programmers to do programming may seem overwhelming for novice programmers. At the same time, this is the reason why programming activities are helpful for a person's growth in their thinking and work skills. When someone learn programming, he or she picks up these various skills such as those needed by someone learning a new foreign language, or like those engineers who are able to solve various problems, or even like those detectives who are able to use their analytical skills to form conclusions from the given evidences. In short, programming is a challenging yet rewarding activities. diff --git a/_Notes/Set.md b/_Notes/Set.md new file mode 100644 index 0000000..317937c --- /dev/null +++ b/_Notes/Set.md @@ -0,0 +1,452 @@ +--- +title: Set +permalink: /notes/set +key: notes-set +layout: article +nav_key: Notes +sidebar: + nav: Notes +license: false +aside: + toc: true +show_edit_on_github: false +show_date: false +--- + +## What is a Set? + +After tuple, list and dictionary, you may be wondering why we need to learn another data type called **set**. Set is a useful data type following set theory in mathematics. Some operations can be performed faster as a set instead of other data type. We will look on some operations that is unique to set. + +First, it is important to define what is a set data type. A set is a collection of **unique** objects. What differentiate a set from a list and a tuple is that it requires its members to be unique. This means that non item in a set is the same. + +There are some similarity between a set and a dictionary. Recall that dictionary's keys must be unique. In a way, you can think of a set as a kind of dictionary that only contains *keys* and *no values* attached to the keys. In fact, to create a set, we use the same curly braces as we use to create a dictionary. + +Let's say, we want to record the dates in January that we exercise. We can create a set in the way shown below. + +```python +>>> january_exercise_dates: set[int] = {1, 3, 4, 5, 31} +>>> type(january_exercise_dates) + +``` + +Notice the curly braces to create the set. Remember that each element of the set must be unique. This means that we have two items that are the same, a set will keep only one of them. This consistency helps a lot for certain type of application and checking. See example below when we recorded date 5th of January two times. + +```python +>>> january_exercise_dates: set[int] = {1, 3, 4, 5, 5, 31} +>>> january_exercise_dates +{1, 3, 4, 5, 31} +``` + +We can also convert other collection data type into a set using `set()` function. This is useful when we want to apply some set operations to these data. For example, we can create the dates in January as another set using the `range()` function and the conversion function `set()`. + +```python +>>> january_dates: set[int] = set(range(1,32)) +>>> january_dates +{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, +11, 12, 13, 14, 15, 16, 17, 18, 19, 20, +21, 22, 23, 24, 25, 26, 27, 28, 29, 30, +31} +``` + +Now, we know what a set is, let's take a look at some operations that makes the set is very useful. + +## Basic Set Operations + +### Adding Item Into a Set + +The first common basic operation is to add an item into a collection. Python has a method called `add()` for a set object to add an item. It is not called `append()` because set too is not ordered and there is no sequence in a set. + +For example, we can add another date when the user exercise in January, say on the 25th. + +```python +>>> january_exercise_dates.add(25) +>>> january_exercise_dates +{1, 3, 4, 5, 25, 31} +``` + +### Removing an Item From a Set + +We can also remove an item from a set. If we just want to remove one item, Python provides `remove()` method. + +```python +>>> january_exercise_dates +{1, 3, 4, 5, 25, 31} +>>> january_exercise_dates.remove(25) +>>> january_exercise_dates +{1, 3, 4, 5, 31} +``` + +Notice that since you can add and remove elements, a set is a mutable data type. + +### Checking if an Item is a Member of a Set + +We can also check if an item is a member of a set using the familiar `in` operator. + +```python +>>> january_exercise_dates +{1, 3, 4, 5, 31} +>>> 31 in january_exercise_dates +True +>>> 25 in january_exercise_dates +False +``` + +With the above code, we can quickly check if the user did exercise on a particular date. + +### Getting the Length of a Set + +Another common thing to do is to find the number of items in a set. We can use again the same function `len()` for a set. + +```python +>>> len(january_dates) +31 +>>> len(january_exercise_dates) +5 +``` +### Common Set Operations + +#### Difference + +But set is useful because of its set operations which was based on set theory in mathematics. The first one is to get a difference between two sets. For example, we want to know the dates that the user do not exercise. What we can do is to take the difference between the dates in the month of January and the dates that the user exercises. + +```python +>> january_dates +{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, +11, 12, 13, 14, 15, 16, 17, 18, 19, 20, +21, 22, 23, 24, 25, 26, 27, 28, 29, 30, +31} +>>> january_exercise_dates +{1, 3, 4, 5, 31} +>>> january_dates - january_exercise_dates +{2, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, +16, 17, 18, 19, 20, 21, 22, 23, 24, 25, +26, 27, 28, 29, 30} +``` + +We have used the `-` operator to get this difference. The difference operator can be visualize using the Venn diagram. + + + +Notice, that we can swap the operands when taking a difference. Let's see what happens if we swap the above operands. + +```python +>>> january_exercise_dates - january_dates +set() +``` + +The result is an empty set. The reason is that january_exercise_dates is smaller than january_dates. In fact, january_exercise_dates is what we call a subset of january_dates. + +If the set is not a subset the difference operation will not return an empty set. Let's take a look at another example. Let's say we have two users who use our exercise cycling app. These are the dates the two users exercise in January. + +```python +>>> john_january: set[int] = {1, 4, 6, 9, 10} +>>> mary_january: set[int] = {4, 9, 11, 12, 31} +``` + +We can take the dates that John exercised but Mary did not as follows. + +```python +>>> john_january - mary_january +{1, 10, 6} +``` + +And we can take the dates that Mary exercised but John did not using the code below. + +```python +>>> mary_january - john_january +{11, 12, 31} +``` + +In fact, we can take a **symmetric difference** of the two sets. + +```python +>>> john_january ^ mary_january +{1, 6, 10, 11, 12, 31} +>>> mary_january ^ john_january +{1, 6, 10, 11, 12, 31} +``` + +These are the dates that John and Mary exercise without the other. + + + +#### Union and Intersection + +But we can find more interesting results using set operations. Let's say we want to know which dates that Mary and John exercise on the same dates. We can use **intersection** operator. + +```python +>>> mary_january & john_january +{9, 4} +>>> john_january & mary_january +{9, 4} +>>> +``` + + + +Maybe the app can arrange the two to exercise together next time? + +We can also get the dates when both Mary and John exercise using **union** operation. + +```python +>>> mary_january | john_january +{1, 4, 6, 9, 10, 11, 12, 31} +``` + +This lists all the dates that both Mary and John exercise. + + + +#### Subset and Superset + +As shown in the previous section, it is useful to know if a set is a subset of another set. To check if a set is a subset of another set, we use `<=` operator. + +```python +>>> january_exercise_dates <= january_dates +True +``` + +We can see that january_exercise_dates is a subset of january_dates. This is why when we substract `january_exercise_dates` with `january_dates` we get an empty set. We can also check if a set is a **proper subset** of another set using `<` operator. + +```python +>>> january_exercise_dates < january_dates +True +>>> january_exercise_dates < january_exercise_dates +False +``` + +A set is a proper subset of another set if it is a subset of the other set and *not* the same as the other set. In the second code above, the result is `False` because the two sets are the same. + + + +In the above image, A is a subset of B. We can also say that B is the superset of A. + +Similarly, we can check if a set is a superset of another set using the opposite operator. + +```python +>>> january_exercise_dates > january_dates +False +>>> january_dates > january_exercise_dates +True +``` + +Here we use the **proper superset** operator but we can also check the usual superset using `>=` operator instead. + +## Identifying When to Use Set + +So when should we use a set? We should use it when we want to use some of the set operations described above such as union, intersection, difference, etc. Most of the times, we may convert another collection data type to a set, perform a set operations and may convert it back to other collection data type. Let's a give an example of this for our cycling app. + +Let's say, we have a dictionary of users and where they stay. To make it simpler, we will indicate the regin of where they stay such as west, east, north, north east, or south and central. + +```python +>>> users_region: dict[str, str] = {'John': 'West', 'Jane': 'East', +... 'Mary': 'West', 'Joe': 'North', 'Brad': 'Central', +... 'Nat': 'East', 'Robin': 'North East'} +>>> regions: list[str] = ['West', 'East', 'North', 'North East', 'South', 'Central'] +``` + +Let's say we want to know which are the regions that our app users stay, we can write the following code. + +```python +>>> active_region = users_region.values() +>>> active_region +dict_values(['West', 'East', 'West', 'North', 'Central', 'East', 'North East']) +``` + +But, we may be more interested to see the active region without any duplication. In this way, we can change this to a set. + +```python +>>> active_region:set[str] = set(users_region.values()) +>>> active_region +{'North East', 'North', 'West', 'Central', 'East'} +``` + +And we can get the region which has no users using the difference operation. + +```python +>>> no_user_region: set[str] = set(regions) - active_region +>>> no_user_region +{'South'} +``` + +First, we convert the list of all the regions into a set and do a difference set operation. We see here that we do not have any users on the South region. So maybe, the marketing group can target those users in the South. + +## Use Case: Matching Users Together + +Let's say our app would like to connect users to cycle together. So we would like to create a dictionary with the region as the key and the user as the values of the dictionary. + +Let's first create a function to generate this new dictionary. + +```python +def generate_region_dict(user_dict: dict[str, str]) -> dict[str, list[str]]: + output: dict[str, list[str]] = {} + # our code here + return output + +users_region: dict[str, str] = {'John': 'West', 'Jane': 'East', +'Mary': 'West', 'Joe': 'North', 'Brad': 'Central', +'Nat': 'East', 'Robin': 'North East'} +regions_dict: dict[str, list[str]] = generate_region_dict(users_region) +``` + +We start with an empty functin and a test. We used our previous user dictionary to drive the test. Notice also that our output dictionary has a string for its key and a list for its value. + +Running mypy and python on the above code gives us the following output. + +```sh +$ mypy matching_users_region.py +Success: no issues found in 1 source file +$ python matching_users_region.py +{} +``` + +The output currently is an empty dictionary. We will not go through the PCDIT steps in detail for this function but please work it out yourself and see if you can write this function yourself. We will just show the final function below. + +```python +def generate_region_dict(user_dict: dict[str, str]) -> dict[str, list[str]]: + output: dict[str, list[str]] = {} + for user in user_dict: + region = user_dict[user] + if region in output: + output[region].append(user) + else: + output[region] = [user] + return output +``` + +The output is shown below. + +```sh +$ mypy matching_users_region.py +Success: no issues found in 1 source file +$ python matching_users_region.py +{'West': ['John', 'Mary'], 'East': ['Jane', 'Nat'], 'North': ['Joe'], 'Central': ['Brad'], 'North East': ['Robin']} +``` + +Since the users are unique and the sequence of the user in a particular region is not significant, it is better to represent this dictionary using a set instead of a list. We can rewrite our function in a different way as follows. + +```python +def generate_region_dict(user_dict: dict[str, str]) -> dict[str, set[str]]: + output: dict[str, set[str]] = {} + for user in user_dict: + region = user_dict[user] + if region in output: + output[region].add(user) + else: + output[region] = {user} + return output +``` + +Notice a few part that changes. First, when a new region is found and it is not in the output dictinary, we created a set instead of a list. + +```python +output[region] = {user} +``` + +Second, instead of using `.append()` when adding a user to a region already existing in the output dictionary, we use `.add()` method which belongs to set object. + +```python +output[region].add(user) +``` + +Lastly, we have changed the type of our returned value. Now, the value of our dictionary is a set. We do this in our function header. + +```python +def generate_region_dict(user_dict: dict[str, str]) -> dict[str, set[str]]: +``` + +We also modified the initialization when creating the output dictionary. + +```python +output: dict[str, set[str]] = {} +``` + +The output now looks as follows. + +```sh +$ python matching_users_region.py +{'West': {'Mary', 'John'}, 'East': {'Jane', 'Nat'}, 'North': {'Joe'}, 'Central': {'Brad'}, 'North East': {'Robin'}} +``` + +With this, we can create some useful functions. The first function, we will create is a function to create a new combined region. For example, let's say we want to see who are the users in the North East and East region. We can create a new dictionary entry called "North East - East" with all the users in these two regions. Let's start by writing the test code. + +```python +ne_e_regions: dict[str, set[str]] = create_combined_region_dict(regions_dict, 'North East', 'East') +print(ne_e_regions) +``` + +Notice that we expect this function to return a new dictionary with the same type. The input to this function is `region_dict` which is the output of the previous function `generate_region_dict()`. + +We can start with the function header and initializing a new dictionary by copying the input argument dictionary. + +```python +def create_combined_region_dict(region_dict: dict[str, set[str]], r1, r2) -> dict[str, set[str]]: + output: dict[str, set[str]] = copy.deepcopy(region_dict) + # your code here + return output +``` + +We used `deepcopy` function to make sure that the dictionary with all the values are copied as a separate distinct object. What we will do now is to add a new entry into the dictionary. However, we will do this only if the region `r1` and `r2` exist in the dictionary. Otherwise, we can just return the dictionary as it is. + +```python +def create_combined_region_dict(region_dict: dict[str, set[str]], r1, r2) -> dict[str, set[str]]: + output: dict[str, set[str]] = copy.deepcopy(region_dict) + if r1 not in region_dict or r2 not in region_dict: + return output + return output +``` + +In the code above, we have added the if-statement to check if the two regions we want to combine exist in the dictionary or not. If one of them does not exist, we will just return the output and exit the function immediately. + +When both regions exists, we can create a new entry. The key will a combined word of the two regions separated by a dash, i.e. `-`. + +```python +output[r1 + ' - ' + r2] = # something +``` + +And the value will be a union of the two sets, i.e. + +```python +region_dict[r1] | (region_dict[r2]) +``` + +So we can write our function as follows. + +```python +def create_combined_region_dict(region_dict: dict[str, set[str]], r1, r2) -> dict[str, set[str]]: + output: dict[str, set[str]] = copy.deepcopy(region_dict) + if r1 not in region_dict or r2 not in region_dict: + return output + output[r1 + ' - ' + r2] = region_dict[r1] | (region_dict[r2]) + return output +``` + +The output of this function given the previous input is shown below. + +```sh +{'West': {'Mary', 'John'}, 'East': {'Jane', 'Nat'}, 'North': {'Joe'}, +'Central': {'Brad'}, 'North East': {'Robin'}, +'North East - East': {'Robin', 'Jane', 'Nat'}} +``` + +Notice that we have a new entry, `North East - East`, in our dictionary. This entry has Robin, Jane and Nat, who are the users in the these two regions. So our app can match these three users to have a cycling activity together. We create any combinations that we want. For example, now we can use the same function to create `North East - East - Central` region. + +```python +ne_e_c_regions: dict[str, set[str]] = create_combined_region_dict(ne_e_regions, 'North East - East', 'Central') +print(ne_e_c_regions) +``` + +In the code above, we feed the previous dictionary containing our first combined region to generate a new region `North East - East - Central`. The output is shown below. + +```sh +{'West': {'Mary', 'John'}, 'East': {'Jane', 'Nat'}, 'North': {'Joe'}, +'Central': {'Brad'}, 'North East': {'Robin'}, +'North East - East': {'Robin', 'Jane', 'Nat'}, +'North East - East - Central': {'Robin', 'Jane', 'Brad', 'Nat'}} +``` + +The best part is that we don't need to create a new function for this. We are able to get all the users in these three regins. Now, Brad from Central region is added into the set. + +## Summary + +In this section, we cover a new data type called set which is very useful when we are dealing with set operations. The key difference is that all the elmenents in the set are unique. Set data type makes it easy to perform some operations that would take us write lines of codes if we were to do using list. Understanding when a set is suitable is an important step in writing a good code. diff --git a/_Notes/StateMachine_ABC.md b/_Notes/StateMachine_ABC.md deleted file mode 100644 index ef836a8..0000000 --- a/_Notes/StateMachine_ABC.md +++ /dev/null @@ -1,539 +0,0 @@ ---- -title: Abstract State Machine Class -permalink: /notes/abstract_sm_class -key: notes-abstract-state-machine -layout: article -nav_key: Notes -sidebar: - nav: Notes -license: false -aside: - toc: true -show_edit_on_github: false -show_date: false ---- - -## Designing StateMachine Class - -As mentioned previously, all state machine have some common characteristics. This motivates us to design an Abstract Base Class for State Machine. In designing an Abstract Base Class for state machine we try to identify what is the thing that all state machines have. We know that all state machine has a **state**. This shall be one of our attributes. We also try to figure out what all state machines *can do* in common. What are the common operations? For our design, we will create three methods that all state machines can do: -- `start()` which is to start the state machine by applying the initial state to the current state of the machine. Before calling the `start()` method, a state machine would have no state and cannot be run. -- `step(inp)` which takes in the current input of the machine and moves the machine to the next state in the next time step. This method returns the output of the machine at that time step. -- `transduce(list_inp)` which takes in the list of all the inputs. This method simply calls the `start()` method and run `step()` for all the input in the list of the input argument. - -Our `StateMachine` class is an Abstract Base Class. This means that some of the methods of this class are waiting for implementation in the child class. This `StateMachine` class itself should not be instantiated. Any state machine instantiation should be done from one of the child classes of `StateMachine`. Why is this so? - -The reason is that every state machine has different **initial state** and different **output function** as well as **next state function**. If two state machines have the same initial state as well as the same output and next state functions, it means that the two machines are equivalent. Therefore, `StateMachine` class cannot provide the detail of what is the initial state of a state machine nor can it provide the functions for the output and the next state of a state machine. These must be defined in the child class definition. The abstract `StateMachine` class only contains those implementations that is found common for all state machines. In defining our Abstract Base Class, we can specify that any implementation of its child class must define the implementation of certain methods. This is done in Python using `@abstractmethod` decorator. In our case, we will force child classses of `StateMachine` to define a method called `get_next_values(state, inp)` where these sub classes can define the implementation of the output and next state functions. This means that this method should return two things, the output and the next state given the current state and the current input. - -Let's draw the UML diagram of the `StateMachine` class. - -drawing - -In this class diagram, we identify that `StateMachine` is an abstract class which requires another sub class to implement some of its definition. We also specifies using `<>` notation that it is the `get_next_values(state, inp)` that the sub class has to define. Note also that the `start_state` must be initialized by the sub-class also since each state machine may have different initial state. - -Recall that `get_next_values(state, inp)` in the child class provides the implementation of both the output and the next state functions. These functions are needed by the `step(inp)` method which is inherited from `StateMachine` class to determine the output and change the current state to the next state. Let's see how we can implement this in Python. - -## Python Implementation of Abstract Base Class - -Python has a module called `abc` which can be used to implement an Abstract Base Class. Any Abstract Base Clas should inherit from `ABC` class inside the `abc` module (yep, the class name uses capital letters while module name uses small letters in Python). - - -```python -from abc import ABC - -class StateMachine(ABC): - pass -``` - -`abc` module provides a decorator `@abstractmethod` which we can use to enforce that some method has to be implemented in the sub class. - - -```python -from abc import ABC, abstractmethod - -class StateMachine(ABC): - - def start(self): - self.state = self.start_state - - def step(self, inp): - ns, o = self.get_next_values(self.state, inp) - self.state = ns - return o - - @abstractmethod - def get_next_values(self, state, inp): - pass -``` - - -```python -# cannot instantiate StateMachine class -# this will generate error - -s = StateMachine() -``` - - - --------------------------------------------------------------------------- - - TypeError Traceback (most recent call last) - - in - 2 # this will generate error - 3 - ----> 4 s = StateMachine() - - - TypeError: Can't instantiate abstract class StateMachine with abstract methods get_next_values - - -If you try to instantiate an abstract class with some abstract method, Python will complain and throws an exception. - -In the above implementation, we also define `start()` method as simply applying the `start_state` as the current `state` value of the machine. Moreover, the `step(inp)` calls `get_next_values(state, inp)`, which should be implemented in the child class, to get the next state and the output given the current state and the current input. The next state is then applied to the `state` and the `step()` method returns the output. - -Thus, `step()` is designed to update the state of the state machine. Thus, when `get_next_values()` is implemented, it must be implemented as a pure function. - -## Creating a LightBoxSM Class - -In our previous notes, we discussed the light box state machine. We simulate it using Object Oriented Programming by defining a class. But now, we have an abstract class from State Machine. How can we create this same light box state machine without re-writing the codes that is already contained in the class `StateMachine`? First, let's recall how we wrote the `LightBox` class without inheriting `StateMachine` class. - - -```python -class LightBox: - def __init__(self): - self.state = "off" - - def set_output(self, inp): - if inp == 1 and self.state == "off": - self.state = "on" - return self.state - if inp == 1 and self.state == "on": - self.state = "off" - return self.state - return self.state - - def transduce(self, list_inp): - for inp in list_inp: - print(self.set_output(inp)) - -``` - - -```python -lb1 = LightBox() -lb1.transduce([0, 0, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 1]) -``` - - off - off - on - on - on - on - on - off - on - off - on - on - off - - -Notice a few things. The class above has an attribute called `state` which store the state of the state machine. The `set_output()` method is similar to `step(inp)` and `get_next_values(state, inp)` methods since they provide the output and affect the state of the machine. - -In using the `StateMachine` class, we will use inheritance and only define two things: -- `start_state` which has to be initialized to the machine initial state value, and -- `get_next_values(state, inp)` method which provides the output and next state functions. - - -```python -class LightBoxSM(StateMachine): - - def __init__(self): - self.start_state = "off" - - def get_next_values(self, state, inp): - - if state == "off": - if inp == 1: - next_state = "on" - else: - next_state = "off" - elif state == "on": - if inp == 1: - next_state = "off" - else: - next_state = "on" - output = next_state - return next_state, output -``` - - -```python -lb2 = LightBoxSM() -lb2.start() -print(lb2.step(0)) -print(lb2.step(0)) -print(lb2.step(1)) -print(lb2.step(0)) -print(lb2.step(0)) -print(lb2.step(0)) -print(lb2.step(0)) -print(lb2.step(1)) -print(lb2.step(1)) -print(lb2.step(1)) -print(lb2.step(1)) -print(lb2.step(0)) -print(lb2.step(1)) - -``` - - off - off - on - on - on - on - on - off - on - off - on - on - off - - -Notice that we have produced the same output using a different way of writing the light box state machine. It would be convenient to have something like the `transduce()` method where we can just put in a list of input to the machines and produce the output. You will work this method in your problem set. - -## Designing a State Machine - -In this section we will discuss how we can design a state machine and write its implementation in Python using some of the code we have provided such as in our `StateMachine` class. - - - -### Is this a state machine? - -The first step is to ask whether our computation is a state machine. To determine the answer for this question, we ask a few question? -- is the output a function of the current input only or it a function of its history or state as well? -- does the computation requires me to remember something else besides knowing the input arguments or can I determine the output solely from the input arguments? - -If your computation output is a function of not only the input arguments but also its history or state of the machine, then it is a state machine. - -### What is the state of the machine? - -Once we know that it is a state machine, we should ask what the state of the machine is. To find out what the state is, we can ask the following questions: -- what is the thing that I need to remember? -- what is the thing I need to know besides my input arguments in order to determine the output? - -### Is the state finite? - -Once we know what the state is, we can determine whether the state is finite or not. Later on we will show that this answer may affect how we determine its output and next state function. - -### Determine the output and next state functions - -If there is finite state, this step involves drawing the state transition diagram based on the problem specification. On the other hand, if the state is not finite, we can use another technique like time step table to try to figure out what is the output and next state function. - -### Implementation - -Once all those steps done, we can implement them in Python. We do this by doing the following: -1. Create a new class inheriting from `StateMachine` class -1. Initialize the `start_state` -1. Define `get_next_values()` - -The implementation of `get_next_values()` follows closely the previous steps. If one has a state transition diagram, every arc and arrow in the diagram is one `if` condition. - -## Using the Steps for LightBoxSM - -Let's use the steps above and see how we come up with the implementation of `LightBoxSM`. First, we ask whether it is a state machine. Since the output not only depends on whether the button is pressed or not (input is 1 or 0) but also on how many time the button has been pressed, we are dealing with a state machine. We can see this since it is not enough for us to determine whether the light is on or off simply from knowing whether the button is pressed or not. We need to know the "other" information. - -Second, we ask what is the state by asking what is the thing that we need to remember. In this case, we need to remember whether currently the light is on or off when the button is pressed. So the status of the light, which is either "on" or "off", is the state of the machine. - -Third, we ask is the state finite? In this case the answer is yes. The reason is that we only identify two states, i.e. "on" or "off". - -Fourth, we draw the state transition diagram based on our problem formulation. A figure of our state transition diagram is shown below. - -drawing - -The states are represented by the circle and we have only two states, i.e. "on" and "off". The arrow direction tells us the **next state function**. Given the current state and the input value, we know what is the next state by looking at the arrow direction. Furthermore, each arc is labelled with an input and its output. So for example, `B=0/OFF` on the most right arrow means that when the current state is "off" and the input 0, the output is "off" for that transition. This provides the **output function**. - -Once we have the state transition diagram, then we can begin to write its Python implementation. First, we need to initialize the `start_state`. In our case "off" is the initial state of the machine. - - -```python -class LightBoxSM(StateMachine): - - def __init__(self): - self.start_state = "off" -``` - -Next, we define the `get_next_values(state, inp)` from the state transition diagram. Notice that there are four arrows in the diagram. So we should expect to have four branches in our if-else statements. - - -```python -class LightBoxSM(StateMachine): - - def __init__(self): - self.start_state = "off" - - def get_next_values(self, state, inp): - - if state == "off": - if inp == 1: - next_state = "on" - else: - next_state = "off" - elif state == "on": - if inp == 1: - next_state = "off" - else: - next_state = "on" - output = next_state - return next_state, output -``` - -In the case here, it happens that the output function is the same as the next state function. But this is not necessarily the case in general. But looking at the state transition diagram will help us to determine both its output and next state functions. - -Notice that `get_next_values(state, inp)` must always return two things: `next_state` and `output`. This is needed by the `step()` method in the `StateMachine` class. - -```python - def step(self, inp): - ns, o = self.get_next_values(self.state, inp) - self.state = ns - return o -``` - -If the `get_next_values()` only returns one thing, Python will throw an exception because it expects a tuple of two items. - - -```python -class LightBoxSM(StateMachine): - - def __init__(self): - self.start_state = "off" - - def get_next_values(self, state, inp): - - if state == "off": - if inp == 1: - next_state = "on" - else: - next_state = "off" - elif state == "on": - if inp == 1: - next_state = "off" - else: - next_state = "on" - output = next_state - # trying returning only one item - return next_state ##, output -``` - - -```python -lb2 = LightBoxSM() -lb2.start() -print(lb2.step(0)) -``` - - - --------------------------------------------------------------------------- - - ValueError Traceback (most recent call last) - - in - 1 lb2 = LightBoxSM() - 2 lb2.start() - ----> 3 print(lb2.step(0)) - - - in step(self, inp) - 7 - 8 def step(self, inp): - ----> 9 ns, o = self.get_next_values(self.state, inp) - 10 self.state = ns - 11 return o - - - ValueError: too many values to unpack (expected 2) - - -Python says "too many values to unpack" and it expects two items from `get_next_values()`. We can give more explanation by catching this error using `try` and `except` block in Python. So our design requires that `get_next_values()` returns both the next state and the output. - - -```python -class LightBoxSM(StateMachine): - - def __init__(self): - self.start_state = "off" - - def get_next_values(self, state, inp): - - if state == "off": - if inp == 1: - next_state = "on" - else: - next_state = "off" - elif state == "on": - if inp == 1: - next_state = "off" - else: - next_state = "on" - output = next_state - - return next_state, output -``` - -## Designing Accumulator Class - -We will show one more example on how to design a state machine. This time, we would like to create an `AccumulatorSM`. This state machine simply accumulates the input. The input to this machine is any number. The following is a typical example of input and output of an accumulator. - -```python -acc = AccumulatorSM() # in the beginning it's zero -acc.step(10) # outputs 10 -acc.step(25) # outputs 35 -acc.step(-5) # outputs 30 -acc.step(11) # outputs 41 -acc.step(-41) # outputs 0 -``` - -Let's follow the step to design this machine. The first step is to ask whether this is a state machine. The answer is affirmative because we cannot determine the output solely from the input. We have to remember what is the accumulated value up to this point in time. - -Second, we ask what the state is. What is the thing we have to remember? In this case is the accumulated value. To determine the output, we not only need to know the input to the machine but also the current accumulated value in the machine. This is the state of the machine. - -Third, we ask whether the state is finite. The answer is negative this time. The reason is that there infinite possible values of the state or the accumulated value. The accumulated value can assume any number like 10, 35, 30, 41, 0, 11, 100, 100.5, etc. It can take any values and so this time, it is not possible to draw the state transition diagram. The reason that it is not possible is that state transition diagram is feasible only if the number of state is finite, which is not the case of an accumulator. - -How can we determine its output and next state function? One useful tool is to fill in the **time step table**. We show such table in the example below. - -| | 0 | 1 | 2 | 3 | 4 | -|----------------|----|----|----|----|-----| -| current state | 0 | 10 | 35 | 30 | 41 | -| current input | 10 | 25 | -5 | 11 | -41 | -| next state | 10 | 35 | 30 | 41 | 0 | -| current output | 10 | 35 | 30 | 41 | 0 | - -In the time step table, the columns are the different time steps like time 0, 1, 2, 3, etc. There are four rows in this table. The first one is the "current state". The value of this row at time step 0 is the initial state of the machine. In our case above, it is 0. The next row gives you the different input at different time steps. Our job now is to fill in the "next state" and the "current output" rows. - -Notice that the next state value at time $t$ is the same as the current state value at time $t+1$. How do we fill up the row "next state"? There is no clear step-by-step answer. One way is to identify first what is the state. Again, the state is the thing that the machine has to remember, the information needed besides the current input in order to determine the output and the next state. - -For example, if we look at time step 2 and ignoring the current state value 35, we should ask, given the current input -5, what is the information I need to get the output 30? Answering this question will help us to identify we need to know the accumulated value up to this point, which is the value of the current state, i.e. 35. Knowing the accumulated value 35 and the current input -5, it is straight forward to obtain the output of that time step by adding the two $output = 35 - 5 = 30$. This output is the new accumulated value in the machine and, therefore, is the information that is needed at time step 3. This is why it is set as the next state value. - -If we can fill up the time step table, hopefully we can try to figure out the next state and the output functions by asking: -- How do I determine or compute the output from the current state and the current input? -- How do I determine or compute the next state value from the current state and the current input? - -In this case, the output and the next state is the same and can be computed simply by adding the two: - -$$\text{output} = \text{current state} + \text{current input}$$ -$$\text{next state} = \text{output}$$ - -Knowing the output and next state functions enable us to write the implementation in Python. Similar to the `LightBoxSM` example, we simply need to specify two things: -- `start_state` and -- `get_next_values()` - - -```python -class AccumulatorSM(StateMachine): - - def __init__(self): - self.start_state = 0 - - def get_next_values(self, state, inp): - output = state + inp - next_state = output - return next_state, output -``` - - -```python -acc = AccumulatorSM() -acc.start() -print(acc.step(10)) # outputs 10 -print(acc.step(25)) # outputs 35 -print(acc.step(-5)) # outputs 30 -print(acc.step(11)) # outputs 41 -print(acc.step(-41)) # outputs 0 -``` - - 10 - 35 - 30 - 41 - 0 - - -## Start Method in StateMachine Class - -It is important to call the `start()` method before running `step()`. The reason is that the state machine's state is initialized with the starting state only at the call of the `start()` method. Let's try what happens if we call `step()` without first calling `start()`. - - -```python -acc = AccumulatorSM() -# acc.start() -print(acc.step(10)) # outputs 10 -print(acc.step(25)) # outputs 35 -print(acc.step(-5)) # outputs 30 -print(acc.step(11)) # outputs 41 -print(acc.step(-41)) # outputs 0 -``` - - - --------------------------------------------------------------------------- - - AttributeError Traceback (most recent call last) - - in - 1 acc = AccumulatorSM() - 2 # acc.start() - ----> 3 print(acc.step(10)) # outputs 10 - 4 print(acc.step(25)) # outputs 35 - 5 print(acc.step(-5)) # outputs 30 - - - in step(self, inp) - 8 def step(self, inp): - 9 try: - ---> 10 ns, o = self.get_next_values(self.state, inp) - 11 except ValueError: - 12 print("Did you return both next_state and output?") - - - AttributeError: 'AccumulatorSM' object has no attribute 'state' - - -Python produces and error saying that the `AccumulatorSM` object has no attribute called `state`. The reason is that the attribute `state` is only created and assigned inside the method `start()`. Recall in our class definition how the method is implemented. - -```python -class StateMachine(ABC): - - def start(self): - self.state = self.start_state -``` - -What's the purpose of this method? Why can't we just initialize the `state` attribute inside `__init__(self)` of the `StateMachine` class. The reason is that `__init__()` is only called one time during object instantiation. On the other hand, creating another method called `start()` allows us to call this method again in the event that we wish to "restart" the state machine back to its original state. Consider the following example. - - -```python -acc = AccumulatorSM() -acc.start() -print(acc.step(10)) # outputs 10 -print(acc.step(25)) # outputs 35 -print(acc.step(-5)) # outputs 30 -print(acc.step(11)) # outputs 41 -print(acc.step(-41)) # outputs 0 - -# restart machine -acc.start() -print(acc.step(100)) # outputs 100 -print(acc.step(10)) # outputs 110 -``` - - 10 - 35 - 30 - 41 - 0 - 100 - 110 - - -Having such initialization outside `__init__()` allows this flexibility of restarting the state of the machine. diff --git a/_Notes/State_Machine.md b/_Notes/State_Machine.md deleted file mode 100644 index 276e7cf..0000000 --- a/_Notes/State_Machine.md +++ /dev/null @@ -1,169 +0,0 @@ ---- -title: State Machine -permalink: /notes/state_machine -key: notes-state-machine -layout: article -nav_key: Notes -sidebar: - nav: Notes -license: false -aside: - toc: true -show_edit_on_github: false -show_date: false ---- - - -## State Machine as a Function - -In the first few weeks, we do computation by defining a function. When the computation return a value, we can write its output in terms of a mathematical equation as shown below. - -$$o = f(i_1, i_2, \ldots)$$ - -In the above expression, we notice that the output is a function of the different inputs, i.e. $i_1, i_2, \ldots$. We call it **Pure Function** if the output only depends on the current input. Mathematically, we can express this as - -$$o_t = f(i_t)$$ - -which says that the output at time $t$ is a function of the input at time $t$. We can express a state machine as a computation machine where the output is not only the function of the current input, but also a function of all the previous inputs. - -$$o_t = f(i_t, i_{t-1}, i_{t-2}, \ldots)$$ - -The above expression says that the current output at time $t$ is a function of the current input at time $t$ and all the previous inputs. Saying that it is a function of the previous input also means that the current output is a function of some *history* of the machine. This history of the machine can be captured as a single entity called a **state** of the machine. Therefore, we can write it as follows. - -$$o_t = f(i_t, s_t)$$ - -The above expression states that the current output is a function of the current input and the current state. The history of the machine is captured in the current state of the machine. - - - -## Examples of Pure Functions - -Let's illustrate the above definition with some Python programs. We will start with a simple example of a function where the output is just the function of the current input. One example is a function to calculate the cartessian distance of a three dimensional coordinate. Given the three input of x, y, and z, the function returns an output of the distance from the origin. - - - - -```python -import math -def distance(x, y, z): - return math.sqrt(x * x + y * y + z * z) -``` - - -```python -assert distance(3, 4, 0) == 5 -``` - -Another example is a function that returns true if all the inputs are true. This is actually what a combinational logic (memoryless) AND gate is. We can implement this combinational logic AND gate as a function as shown below. - - -```python -def multiple_and(*args): - for truth in args: - if not truth: - return False - return True - -``` - - -```python -assert not multiple_and(True, True, False) -assert multiple_and(True, True, True, True) -``` - -The two functions above has an output that is a function of only the current input. How about a state machine? What kind of computer program can be called a state machine? - -## Objects and State Machine - -Let's begin by creating a simple state machine called a light box. A light box has one input which is a button. When the input is pressed, a value of integer 1 is sent to the machine. When the input is not pressed, a value of integer 0 is registered. The machine only has one single button. To turn on the light, one has to press the button one time. To turn off the light, the same button has to be pressed one more time. - -Notice that now the output is not just a function of the current input. The input is 1 for both to turn on and off the light. We have to know how many time the input is 1 to determine whether the light is on or off. If the input is 1 only one time and assuming the light is off initially, we should turn on the light. But if the input is 1 for the second time, we should turn off the light. - -How can we write a program to simulate this state machine? - -One easy way is to use Object Oriented Programming. In this case, the machine is an object and we try to identify, what are its attribute and its methods. In our design, we will have an attribute called `state` to remember whether the light is currently on or off. We then have a method to set the output of the state machine based on the input. Lastly, we have another method called `transduce()` that takes in an argument which is a list of the input for different time steps. We write our class definition below. - - -```python -class LightBox: - def __init__(self): - self.state = "off" - - def set_output(self, inp): - if inp == 1 and self.state == "off": - self.state = "on" - return self.state - if inp == 1 and self.state == "on": - self.state = "off" - return self.state - return self.state - - def transduce(self, list_inp): - for inp in list_inp: - print(self.set_output(inp)) - -``` - - -```python -lb = LightBox() -lb.transduce([0, 0, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 1]) -``` - - off - off - on - on - on - on - on - off - on - off - on - on - off - - -The test code in the above example call the transduce method with a list of integer values. The input is 1 when the button is pressed and 0 otherwise. We can see that when the button is pressed at time step 3, the output changes from "off" to "on". Notice that initially the output is "off". At time step 8, we have 4 presses of the input buttons and we can see the output is toggled four times. At the last time step, the button is pressed again and the light is switched from "on" to "off". We have implemented our first state machine. - -In general, we can say that all objects in a computer program is a state machine where the values of the objects' attributes define the state of those objects. - - - -## Next State Function - -In the previous example of light box state machine, it happens that the output is always the same as the state. But this is not necessarily the case. The output can be different from the state. So in general, a state machine has two functions: the output function and the next state function. - -$$o_t = f(i_t, s_t)$$ - -$$s_{t+1} = f(i_t, s_t)$$ - -The first one is the output function which we have discussed. The second mathematical expression expresses the **next state function** of the state machine. It simply says that the next state of the machine is a function of the current input and the current state. - -To illustrate that the output function can be different from the next state function. Let's consider a new state machine called the Coke Machine. The figure below shows what we call as the **state transition diagram**. There are many possible design of a Coke machine and the one below is just one particular example of our design choice. - -![jpeg](/assets/images/week12/coke_sm.jpeg) - - -Each directed arc in the state diagram is labelled as $x/y$ where $x$ denotes the input received and $y$, the output generated. For example, the arc that connects state 0 to state 1 thatโ€™s labelled `50/(50, โ€™--โ€™,0)` means that when the dispenser receives 50ยข (50 before the /) in state 0, it moves to state 1 and generates an output of `(50, โ€™--โ€™,0)`. This tuple of values in the output indicates that the dispenser display shows 50 which is the amount deposited by the user, no coke has been dispensed yet as indicated by `--`, and no change has been returned to the user as indicated by the last entry which is a 0. -The machine accepts only 50ยข and one dollar (100ยข) coins. It has a display that shows how many cents have been deposited. - -The above state machine has only two states, 0 and 1. State 0 is when there is no coin deposited inside the machine while state 1 is when there is a 50ยข deposited inside the machine. The output, however, has more than two possible outcomes. The output is expressed as a tuple of three values (x, y, z): -- the first part, x: is the amount of money inside the machine. -- the second part, y: is the status whether coke is dispensed or not. -- the third part, z: is the change output of the machine. - -The state transition diagram is a visual way of defining the output function and the next state function. By looking at the state transition diagram and knowing where the current state is, one can determine both the output and the next state of the machine. - - -## Initial State - -What is lacking in the above diagram is the information of the initial state. A state transition diagram should also contains the information which is the initial state of the machine. In the above case, it makes sense to set state 0 as the initial state where there is no money being deposited into the machine. - -Without information on the initial state, we cannot determine either the output nor the next state. It is important, therefore, to indicate which is the initial state of the machine. - -## Abstracting a State Machine - -As we can see that all state machines must store information about their *states*. For example, the light box needs to remember whether currently it is on or off and a coke machine needs to remember whether it contains a 50 cents inside the machine or not. Moreover, every state machine must be able to determine its output and next state from the current input and the current state. This is true for both light box and coke machine. The difference is that in light box machine, its output function is the same as the next state function. On the other hand, the coke machine output function is not the same as the next state function. Regardless of these two functions, a state machine should be able to move to the next state at every time step. Since all state machines have some common property and functionality, it is possible to abstract these in OOP with an Abstract Base Class. We will show this in the next lesson. diff --git a/_Notes/State_Space_Search.md b/_Notes/State_Space_Search.md deleted file mode 100644 index 2bbecf2..0000000 --- a/_Notes/State_Space_Search.md +++ /dev/null @@ -1,186 +0,0 @@ ---- -title: State-Space Search -permalink: /notes/state_space_search -key: notes-state-space-search -layout: article -nav_key: Notes -sidebar: - nav: Notes -license: false -aside: - toc: true -show_edit_on_github: false -show_date: false ---- - - -Previously we have learnt on how we can find data from a graph data structure. We implemented breadth-first search and explore depth-first search algorithm. We can look into this problem from a different angle as a state-space search. Instead of thinking about nodes and edges, we can see it as *states* and *transitions*. Each node can be thought of as one state and each edge can be thought of as a transition from one state to anther state. We can then apply graph algorithm for our state machines. - -But what does it mean of doing a state-space search? Remember that we attach every transition from one state to another state with an *input* that moves that state machine from the current state to the next state. Doing a state-space search allows us to find a path or *sequence of inputs* that bring us from a starting state to a goal state. This means that if I know my goal state of my machine, I can do a search what are the inputs needed to reach that state. This can be used to make our state machine more intelligent through *planning*. - -## Defining State-Space Search - -Let's first define our problem. We can model this state-space search problem as follows. Given the following information: -- a set of states the system can be in; -- a starting state; -- a goal test, which is a procedure that can be applied to any state, and returns the True if that state is the goal state; -- a successor function, which takes a state and an action as input, and returns the new state that results from taking the action in that state; -- and a legal action list, which is just a list of actions that can be executed in this domain -the problem is to find a sequence of input that brings us to the goal state given the starting state. - -Let's take a look an an example. Consider the case where we have the following states: - -drawing - -We do not draw any arrow in the above diagram to simplify the drawing as we assume that the transition is bi-directional. This means that there is a transition from S to A and from A to S. - -In the above example, we have the set of states: - -$$states = \{`S`, `A`, `B`, `C`, `D`, `E`, `F`, `G`, `H`\}$$ - -We also need to know the starting state, so let's say state 'S' is the starting state. The goal test is a function that takes in a state as an input and returns True if that state is the goal state. So if we want to reach G starting from S, we can write the following goal test. - -```python -def goal_test(state): - return state == 'G' -``` - -We can also write it as a lambda function in Python as follows. - -```python -lambda state: state == 'G' -``` - -The legal actions in this domain can be integers values like $0, 1, \ldots, n-1$, where $n$ is the maximum number of successors in any of the states. The maximum number of successors simply means the maximum number of degrees in the graph. We can find this number by looking at the node (or state) that has the largest number of edges. In this case, state 'D' has the largest number of edges, i.e. 4. So the legal actions are integer values: 0, 1, 2, 3. We can assign, for example, the following transitions: -- input 0: transition from D to A -- input 1: transition from D to B -- input 2: transition from D to F -- input 3: transition from D to H - -Therefore, we also need a kind of transition maps. If our legal input is integer values, we can simply use a list where the index matches the transition. We can write our transition map as follows. - -```python -statemap = {'S': ['A', 'B'], - 'A': ['S', 'C', 'D'], - 'B': ['S', 'D', 'E'], - 'C': ['A', 'F'], - 'D': ['A', 'B', 'F', 'H'], - 'E': ['B', 'H'], - 'F': ['C', 'D', 'G'], - 'G': ['F', 'H'], - 'H': ['D', 'E', 'G']} -``` -Notice in the above dictionary that we use list in the same sequence for state D, i.e. - -```python -'D': ['A', 'B', 'F', 'H'], -``` - -This allows us to have transition as specified above. For example, -```python -statemap['D'][0] # returns state A -statemap['D'][1] # returns state B -statemap['D'][2] # returns state F -statemap['D'][3] # returns state H -``` - -Given the above dictionary, we can simply write our successor function as follows. - -```python -def statemap_successor(state, action): - return statemap[state][action] -``` - -We may need some additional test to ensure that if an action that does not exist from that current state, it will just remain in the current state. For example, state 'G' has two transitions, we should expect the following. - -```python -print(statemap_successor('G', 2)) # output 'G' -``` - -The above code should output 'G' because there is only two transition and so action 2 which index the third transition does not exist. - -## Search Trees - -Our state-space search can be represented as a search tree having the starting state as its root. For example, if we want to search path from state S to state D, we can draw the following search tree. - -![](/assets/images/week12/state_search_trees.jpeg) - -In the above tree, we label the edges with the input action that one takes from one state to another state following the `statemap` dictionary in the previous section. If we can build this tree, we can find the path from S to D and we know that we need to take the following actions: -1. Take 0 from S to reach B -1. Take 1 from B to reach D -or we can also take the following sequence of actions: -1. Take 1 from S to reach A -1. Take 2 from A to reach D - -## Class SearchNode - -We can facilitate this search by creating a class called `SearchNode` that contains the information needed to build the search Trees. The UML diagram for `SearchNode` is shown below. - -drawing - -The class has three attributes: -- state, which identifies the node for the particular state it represents in the tree -- action, which stores the action it takes from the parent to reach the current node -- parent, which stores the reference to the parent node in the search tree - -The class has three methods: -- `path()` which returns the path from the root of the tree to the current node -- `in_path(state)` which takes in a state and check if that state is in the path from the root to the current node -- `__eq__(other)` which allows us to use the equality operator in Python to check if two nodes are equal - -You will work on the implementation of this class in your Problem Set. - -## State Machine for State-Space Search - -We have been looking into this search problem from the perspective of state machine. This implies that we can create a state machine class to represent a state machine that does state-space search. This class, however, has unique requirements as you need to make sure you have enough information for the problem. This means that you may want to ensure that such class must provide information about the following: -- `statemap`, which gives you the transition relationship from one state to another with respect to the legal input -- `legal_inputs`, which is a set of legal inputs in this domain. - -We can enforce this in Python using the Abstract Base Class and creating an **abstract property**. We can, thus, define the following class: - -```python -from abc import abstractmethod - -class StateSpaceSearch(StateMachine): - @property - @abstractmethod - def statemap(self): - pass - - @property - @abstractmethod - def legal_inputs(self): - pass -``` - -In the above definition, `StateSpaceSearch` class inherits from `StateMachine` class and it adds the required property `statemap` and `legal_inputs`. Any state machine class implementing `StateSpaceSearch` now must define these two properties and its getter method. For example, we can implement the `statemap` property as follows: - -```python -class MapSM(StateSpaceSearch): - - def __init__(self, start): - self.start_state = start - - @property - def statemap(self): - statemap = {"S": ["A", "B"], - "A": ["S", "C", "D"], - "B": ["S", "D", "E"], - "C": ["A", "F"], - "D": ["A", "B", "F", "H"], - "E": ["B", "H"], - "F": ["C", "D", "G"], - "H": ["D", "E", "G"], - "G": ["F", "H"]} - return statemap -``` - -Notice that the `statemap` must be defined in the child class that implements `StateSpaceSearch` because such mapping transition information can be different from one state machine to another. This cannot be defined in the base class. - -Similarly, the computed property `legal_inputs` can take in the information from `statemap` and return a set of the legal inputs. Though we may think that the code to create such a set is the same for all state-space search, it actually depends on how one implements the `statemap` property. If one uses a list as in our case, such set will be a set of integer values. But if one uses a dictionary, the set `legal_inputs` may be a set of other data types used in the key of that dictionary. - -In this state machine the `start_state` is the starting state of our search problem. For example, state 'S' should be initialized as our `start_state` following the previous examples. - -We also need to define our `get_next_values(state, inp)` method. This method should return the next state and the output of our search transition. In our state-space search, the output is usually the same as the next state, which is what state should the machine go to given the current state and the input action. In fact, this function is exactly what a successor function `statemap_successor(state, action)` does in our previous discussion. This means that you can implement your `get_next_values(state, inp)` method using the information you have from `statemap` and `legal_inputs`. Remember to ensure that if the input is not valid for that current state, it should remain in the current state. And now you have a complete state machine to do state-space search. - -You can write your breadth-first search algorithm and makes use of the `get_next_values()` of the state machine to find a path from the starting state to the goal state. You will do this in your Problem Set. diff --git a/_Notes/String.md b/_Notes/String.md new file mode 100644 index 0000000..4cc7a63 --- /dev/null +++ b/_Notes/String.md @@ -0,0 +1,451 @@ +--- +title: String +permalink: /notes/string +key: notes-string +layout: article +nav_key: Notes +sidebar: + nav: Notes +license: false +aside: + toc: true +show_edit_on_github: false +show_date: false +--- + +## Recap: String Data + +In our lesson [Basic Data Types]({{ "/notes/basic-data-types" | relative_url }}), we have introduced `string` data to represent text. In a way, `string` data is different from numbers like `int` and `float` because it consists of a sequence of **characters**. Though numbers can be thought of like a sequence of digits, however, we tend to manipulate and treat numbers as a whole. On the other hand, string is rather different. There are many occasions that we want to manipulate some of the substrings in a particular string or even some of its characters. For example, in many text processing, we want to remove the leading and trailing whitespaces in the string as they may not be meaningful for our data processing. Those whitespaces are part of a string. Therefore, it is important to be able to manipulate not only the string as a whole but also its substrings and characters inside the string. In this way, a string is like a **collection** of characters which we want to process. We will introduce another *collection* data type in the future, but for now, string is a good way to introduce how computing processes these kind of collection-like data. + +Let's recap again on how we can create `string` data. We can create string data using either a single quote or a single double quote. + +```python +str1: str = 'This is a string data.' +str2: str = "This is another string data." +``` + +We have also discussed on why Python have these two kinds of string delimiters. Other programming language may only have one way of creating a string data. For Python, it is one simply way to create a string whenenver there is an apostrophe or quotes inside that string. See example, below. + +```python +print("How're you?") +print('The actor exclaimed, "To be or not to be"') +``` + +In the the first example, we have a single apostrophe in the string. Therefore, we use the double quotes as the string delimiter. It's the other way around for the second example which contains quotes in the string data. + +We can create multi-line string data using **triple** single quotes or **triple** double quotes. + +```python +data: str = ''' +Two roads diverged in a yellow wood, +And sorry I could not travel both +And be one traveler, long I stood +And looked down one as far as I could +To where it bent in the undergrowth; + +Then took the other, as just as fair, +And having perhaps the better claim, +Because it was grassy and wanted wear; +Though as for that the passing there +Had worn them really about the same, + +And both that morning equally lay +In leaves no step had trodden black. +Oh, I kept the first for another day! +Yet knowing how way leads on to way, +I doubted if I should ever come back. + +I shall be telling this with a sigh +Somewhere ages and ages hence: +Two roads diverged in a wood, and Iโ€” +I took the one less traveled by, +And that has made all the difference. + +-- by Robert Frost +''' +``` + +We can do the same with a triple double quotes. +```python +data: str = """ +โ€œHopeโ€ is the thing with feathers - +That perches in the soul - +And sings the tune without the words - +And never stops - at all - + +And sweetest - in the Gale - is heard - +And sore must be the storm - +That could abash the little Bird +That kept so many warm - + +Iโ€™ve heard it in the chillest land - +And on the strangest Sea - +Yet - never - in Extremity, +It asked a crumb - of me. + +-- by Emily Dickinson +""" +``` + +Notice that in the above data, it contains both double quotes in the first line and a single quote in `I've heard...`. So triple quotes can handle those in a single string without any issue. + +## Basic String Operations + +Previously, in our lesson [Basic Operators]({{ "/notes/basic-operators" | relative_url }}), we have also introduced two simple operators that can be used with string data: the concatenation `+` operator and the duplication `*` operator. Let's review it again here. + +We can concatenate two strings using the `+` operator. + +```python +>>> first_name: str = "John" +>>> last_name: str = "Wick" +>>> full_name: str = first_name + last_name +>>> print(full_name) +JohnWick +``` + +Oops. The concatenated string does not have a space. But we can fix that by concatenating a space in between. + +```python +>>> full_name: str = first_name + " " + last_name +>>> print(full_name) +John Wick +``` + +This kind of operation is useful in many applications since usually the user profile is stored as first name and last name. You can display the full name in the profile by concatenating the two strings. Similarly, with data like home address where you need to concatenate the road address, unit number and its postal code. + +We have also introduced the duplication operator `*`. For example, we can create an ASCII artwork as below. + +```python +data: str = 3 * " " + "*" + "\n" +data += 2 * " " + 3 * "*" + "\n" +data += 1 * " " + 5 * "*" + "\n" +data += 3 * " " + "*" + "\n" +data += 3 * " " + "*" + "\n" +print(data) +``` + +You can see the output by running it in Python Tutor below. + + + +We used a few operators here. The most common one is actually the assignment operator `=`. In the first line, we created the first string and assign it to the name `data`. How did we create the first string? We have the following line. + +```python +data: str = 3 * " " + "*" + "\n" +``` + +In this line of code, we duplicate a single space string `" "` three times and concatenate it with a single asterisk. At the end we added a **newline character** `"\n"` so that the next string added will be printed into a new line. The second line added the next line to the string. + +```python +data += 2 * " " + 3 * "*" + "\n" +``` + +In this code, we use the compound operator `+=` which is equivalent to `data = data + something`. That something is the expression on the right hand side. It does a few thing. First it duplicates the space two times and concatenate with the asterisk that is duplicated three times (`***`). Lastly, it is concatenated with a newline character again. By now, you should be able to guess what the other lines do. The result is a simple christmas tree. + +``` + * + *** + ***** + * + * +``` + +On top of that, we also learned that we can compare strings in our lesson [Boolean Data]({{ "/notes/boolean-data" | relative_url }}). We can use the relational operators such as `<`, `<=`, `>`, `>=`, `==`, `!=`. In many cases, we are actually interested to compare if two strings are equal or not equal. A common example is in many website form or login when we want to compare whether the user is in a database. + +```python +>>> name_entered: str = 'John the Wick' +>>> name_in_database: str = 'John Wick' +>>> print(name_entered == name_in_database) +False +``` + +## Collection Operators + +Now, we will introduce a few new operators that works in a collection-like data type. We mention that though string data can be considered as a collection of characters inside that string. There are some common operations that we usually do with collection-like data. + +### Check If Substring is in a String + +The first one maybe is simply to check if some item is inside a collection. In the case of `string` data, we may want to check if a character is inside a string or if a substring is inside a string. In this case we use the `in` operator. This operator evaluates to a boolean data because it is either true, when the substring is inside the string, or false, when the substring is not inside string. + +```python +>>> char: str = 'a' +>>> vowel: str = 'aiueo' +>>> print(char in vowel) +True +``` + +Similarly, we can do the same for a substring. + +```python +>>> first_name: str = 'John' +>>> full_name: str = 'John Wick' +>>> print(first_name in full_name) +True +``` + +### Getting the Length of the String + +It is very useful to know what is the length of a collection. This means like how many items are there in a list. In the case of string data, we are interested to know what is the length of the string or how many characters are there in the string. This is done simply using the `len()` built-in function provided by Python. + +```python +>>> name: str = "John Wick" +>>> print(len(name)) +9 +``` + +You can count manually to verify whether "John Wick" has nine characters. + + +### Getting an Element + +Another common operation in collection-like data is to get an element from the collection. In our string data, we may want to get the first character or the last character. To do this we will use the **bracket** operator where we specify the **index** inside the bracket. The index starts from 0 in Python. + +| index | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | +|-----------|---|---|---|---|---|---|---|---|---| +| character | J | o | h | n | | W | i | c | k | + +We can get the different characters by specifying the index inside the bracket operator. The bracket operator is also called the **Get Item** operator. + +```python +>>> name: str = "John Wick" +>>> print(name[0]) +J +>>> print(name[5]) +W +>>> print(name[8]) +k +``` + +You can also use **negative indexing** with the bracket operator. + +| index | -9 | -8 | -7 | -6 | -5 | -4 | -3 | -2 | -1 | +|-----------|----|----|----|----|----|----|----|----|----| +| character | J | o | h | n | | W | i | c | k | + +```python +>>> name: str = "John Wick" +>>> print(name[-9]) +J +>>> print(name[-4]) +W +>>> print(name[-1]) +k +``` + +Notice that we can get the same characters either using the positive indexing or the negative indexing. It is, however, very convinient to get the last character using the index `-1`. Otherwise, we will need to know the length of the string or the collection. The last character is always **the length of the string minus one**. + +```python +>>> name: str = "John Wick" +>>> print(name[len(name) - 1]) +k +>>> print(name[-1]) +k +``` + +The reason that the last character is always length of string minus one is that Python starts its indexing from 0. As shown in the table above, the last character has the index of 8 when the length of string is 9. + +### Getting a Substring from a String + +Not only we can get a character from a string, we can also slice a substring from a string. This **slicing** operation makes use of the same **Get Item** operator (or the bracket operator). The only difference is that the argument inside the bracket is a slice that contains double colon. + +```python +[start:end:step] +``` + +The only point to take note is that **end index** is excluded from the sliced substring. To illustrate. Let's put back here our table and try a few slicing operations. + +| index | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | +|-----------|---|---|---|---|---|---|---|---|---| +| character | J | o | h | n | | W | i | c | k | + +```python +>>> name: str = "John Wick" +>>> print(name[0:3]) +Joh +>>> print(name[3:6]) +n W +``` + +In the first slice, we start with index `0` and end at index `3`. Notice that index 3 refers to the letter `n` in `John`. However, what is printed is only `Joh` without the `n` character. This is to emphasize that the ending index is **excluded** in the slice. + +What's the reason for this? Python developers find it easier to obtain the length of the slice from the two indices. When we have `name[0:3]`, we can immediately subtract the two indices $3-0 = 3$ which gives us the length of the sliced substring `Joh`. + +We can see similar behaviour in `name[3:6]`. In this case, index 3 is on character `n` while index 6 is on character `i` after the letter `W`. However, the output simply prints `n W` which contains three character as you can calculate from the two indices. + +When the slicing starts from the beginning or all the way to the end, you can remove the index from the slice. Python will use those first index and all the way to the last element as default values. See the two examples below. + +```python +>>> print(name[:4]) +John +>>> print(name[5:]) +Wick +``` + +You can also provide steps in the slicing. So if you would like to get every other characters from the string, you can do something like the following. + +```python +>>> name: str = "John Wick" +>>> print(name[::2]) +Jh ik +>>> print(name[1::2]) +onWc +``` + +The first slicing is from the beginning to the end with a step of two characters. Therefore, it starts from the first character (`J`) and then the third character (`h`) and so on. The second example put the starting index as 1 which is the second character. The second output gives you characters from the second to the end with every two steps. + +You can use negative indexing in slicing though it may become rather confusing. Let's see one example here. + +```python +>>> print(name[-1:-10:-1]) +kciW nhoJ +>>> print(name[::-1]) +kciW nhoJ +``` + +In the first insance, we use the starting index to be -1 which is the last character and all the way to the character position before the first character. See from above table that the first character is at -9 index. Here, we put the ending index to be -10 which is one before the first character. We need to do this because the ending index is excluded. Notice also that we put the step to be -1. In the second example, we do exactly the same without specifying the starting and ending index. Python can figure out that you want to take the whole string and reverse it from the negative index in the step argument. + +Use negative index sparingly and only when it clarifies what you are trying to do. Try to use positive indexing as it is clearer in many cases. + +## String is Immutable + +Different programming language may implement string data type differently. Python makes its `string` data type to be **immutable**. Immutable means that you cannot change it. Once you create a string, you cannot edit it or change it. + +```python +>>> name: str = "John Wick" +>>> name[0] = 'Z' +Traceback (most recent call last): + File "", line 1, in +TypeError: 'str' object does not support item assignment +>>> +``` + +Notice that the error says string data does not support item assignment. The assignment operator does not work with string data because string is immutable. How can you change the string then? The answer is that you cannot. But you can create a new string. + +For example, in the above case when we want to change the first character to `'Z'`, we can do the following. + +```python +>>> name = 'Z' + name[1:] +>>> print(name) +Zohn Wick +``` + +Here, we managed to change the name from John Wick to Zohn Wick. What is important here is that `name` variable points to a new string. In order to illustrate this, we will use the built-in function `id()` to check its identity which depends on where it is located in the memory address. + +```python +>>> name:str = "John Wick" +>>> id(name) +140450800593904 +>>> name = 'Z' + name[1:] +>>> id(name) +140450800593840 +``` + +Notice that the variable `name` has two different identities number. + +A number of data types in Python are actually immutable such as `int` and `float`. You can check it out in the same manners. + +```python +>>> a: int = 1 +>>> id(a) +4544481632 +>>> a = 2 +>>> id(a) +4544481664 +>>> b: int = 3. +>>> id(b) +140450263065232 +>>> b = 3.14 +>>> id(b) +140450531292976 +``` + +What happens with every assignment is that Python creates a new binding to a new `int` or `float` objects. This is what Python does with `string` object as well. + +## String Formatting + +There are several ways to format string data. This is particularly useful when you have a combination of string with other data type to be displayed as string literals. In this lesson, we will introduce Python's formatted string literals to achieve this. + +Python's formatted string literals start with `f` or `F` before the string quotes. Inside this string, you can write Python's expression within `{}`. Let's see some example. + +```python +>>> name: str = "John Wick" +>>> greeting: str = f"Good day, {name}!" +>>> print(greeting) +Good day, John Wick! +``` + +Notice that we put the variable name as the expression within the `{}`. Not only variables, you can actually put any Python's expression there. For example, we can call the `len()` function inside this formatted string literals. + +```python +>>> reply: str = f"My name, {name}, has {len(name)} characters." +>>> print(reply) +My name, John Wick, has 9 characters. +``` + +You can format how you want to display the evaluated data inside the `{}` in several ways. For example, you can specify the minimum width. You do this by putting `:X` after the expression where `X` is a number specifying the width. + +```python +>>> reply: str = f"My name, {name:15}, has {len(name):10d} characters." +>>> print(reply) +My name, John Wick, has 9 characters. +>>> print(reply) +My name, John Wick , has 9 characters. +``` + +You may notice some extra space in `John Wick ` at the end of the name and some extra space in ` 9` before the number 9. What happens is that Python reserves 15 spaces for `name` and 10 spaces for `len(name)`. You may also notice that for string data it is left-aligned by default while for number data is right-aligned. You can change this using the alignment format specifier, i.e. `<` for left-aligned and `>` for right-aligned. + +```python +>>> reply: str = f"My name, {name:>15}, has {len(name):<10d} characters." +>>> print(reply) +My name, John Wick, has 9 characters. +``` + +You may notice there is a `d` in `10d`. That type specification can be used to format various number data type. You can change it to binary, hex, or octal. The complete table is given below. + +| Type | Meaning | +|------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| 'b' | Binary format. Outputs the number in base 2. | +| 'c' | Character. Converts the integer to the corresponding unicode character before printing. | +| 'd' | Decimal Integer. Outputs the number in base 10. | +| 'o' | Octal format. Outputs the number in base 8. | +| 'x' | Hex format. Outputs the number in base 16, using lower-case letters for the digits above 9. | +| 'X' | Hex format. Outputs the number in base 16, using upper-case letters for the digits above 9. In case '#' is specified, the prefix '0x' will be upper-cased to '0X' as well. | +| 'n' | Number. This is the same as 'd', except that it uses the current locale setting to insert the appropriate number separator characters. | +| None | The same as 'd'. | + +You may guess that for floating point number, there are a number of ways you want to display such as the number of decimal points or in a scientific notation instead. In this case the format is `expression:W.dt`, where: +- `expression` is the expression to be displayed in the string literal. +- `W` is the total width including the decimal point and its decimal numbers. +- `d` is the number of decimal point to be displayed. +- and `t` is the format type. + +Below are some examples. + +```python +>>> from math import pi +>>> f"Pi number is: {pi}" +'Pi number is: 3.141592653589793' +>>> f"Pi number is: {pi:10.2f}" +'Pi number is: 3.14' +>>> f"Pi number is: {pi:5.2e}" +'Pi number is: 3.14e+00' +``` + +The table for the floating point type is shown below and was taken from Python's [Format String Syntax documentation](https://docs.python.org/3/library/string.html#formatstrings). + +| Type | Meaning | +|------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| 'e' | Scientific notation. For a given precision p, formats the number in scientific notation with the letter โ€˜eโ€™ separating the coefficient from the exponent. The coefficient has one digit before and p digits after the decimal point, for a total of p + 1 significant digits. With no precision given, uses a precision of 6 digits after the decimal point for float, and shows all coefficient digits for Decimal. If no digits follow the decimal point, the decimal point is also removed unless the # option is used. | +| 'E' | Scientific notation. Same as 'e' except it uses an upper case โ€˜Eโ€™ as the separator character. | +| 'f' | Fixed-point notation. For a given precision p, formats the number as a decimal number with exactly p digits following the decimal point. With no precision given, uses a precision of 6 digits after the decimal point for float, and uses a precision large enough to show all coefficient digits for Decimal. If no digits follow the decimal point, the decimal point is also removed unless the # option is used. | +| 'F' | Fixed-point notation. Same as 'f', but converts nan to NAN and inf to INF. | +| 'g' | General format. For a given precision p >= 1, this rounds the number to p significant digits and then formats the result in either fixed-point format or in scientific notation, depending on its magnitude. A precision of 0 is treated as equivalent to a precision of 1. The precise rules are as follows: suppose that the result formatted with presentation type 'e' and precision p-1 would have exponent exp. Then, if m <= exp < p, where m is -4 for floats and -6 for Decimals, the number is formatted with presentation type 'f' and precision p-1-exp. Otherwise, the number is formatted with presentation type 'e' and precision p-1. In both cases insignificant trailing zeros are removed from the significand, and the decimal point is also removed if there are no remaining digits following it, unless the '#' option is used. With no precision given, uses a precision of 6 significant digits for float. For Decimal, the coefficient of the result is formed from the coefficient digits of the value; scientific notation is used for values smaller than 1e-6 in absolute value and values where the place value of the least significant digit is larger than 1, and fixed-point notation is used otherwise. Positive and negative infinity, positive and negative zero, and nans, are formatted as inf, -inf, 0, -0 and nan respectively, regardless of the precision. | +| 'G' | General format. Same as 'g' except switches to 'E' if the number gets too large. The representations of infinity and NaN are uppercased, too. | +| 'n' | Number. This is the same as 'g', except that it uses the current locale setting to insert the appropriate number separator characters. | +| '%' | Percentage. Multiplies the number by 100 and displays in fixed ('f') format, followed by a percent sign. | +| None | For float this is the same as 'g', except that when fixed-point notation is used to format the result, it always includes at least one digit past the decimal point. The precision used is as large as needed to represent the given value faithfully. For Decimal, this is the same as either 'g' or 'G' depending on the value of context.capitals for the current decimal context. The overall effect is to match the output of str() as altered by the other format modifiers. | + + +## Summary + +In this section, we have dived slightly deeper on `string` data type. We review how to create string literals and some of basic operations that are common to string data. String can be thought of as a collection-like data where we can process the sequence of characters inside this string. In this lesson, we focus more on how to slice some substring from a given string. In the next lesson, we will learn how to iterate over each character. At the end, we emphasise that string is immutable data type in Python. This means that we cannot change it once it is created in the memory. We also showed some simple way to format string using Python's formatted string literals. \ No newline at end of file diff --git a/_Notes/Tuple.md b/_Notes/Tuple.md new file mode 100644 index 0000000..b3f624d --- /dev/null +++ b/_Notes/Tuple.md @@ -0,0 +1,303 @@ +--- +title: Tuple Data +permalink: /notes/tuple +key: notes-tuple +layout: article +nav_key: Notes +sidebar: + nav: Notes +license: false +aside: + toc: true +show_edit_on_github: false +show_date: false +--- + +## What is a Tuple? + +This lesson and the next focuses on data structure that is available in Python. More importantly, we will introduce you to collection-like data structure. In the previous lesson, we have been dealing with only basic data structure such as `int`, `float`, `bool` and `string`. When we are dealing with `string`, we noted that we can think of `string` as a sequence of characters. We can then process each character by iterating over the string. Computer is very efficient when working with this kind of data when it has do the same computation again and again. All programming languages have some ways to implement iterative structure as it is one of the ways that makes programming to be powerful. + +In this section and the next, we will begin to work with more collection-like data type. In this section, we will introduce `tuple`. On the other hand, the next section will deal with `list`. + +You may not realize it, but you have actually dealt with tuple when we discuss about function that returns multiple output. Let's put the function again here for us to recall. + +```python +import math +def calculate_speed(diameter: float, tire_size: float, + chainring: int, cog: int, + cadence: int) -> tuple[float, float]: + ''' + Calculates the speed from the given bike parameters and cadence. + + Parameters: + diameter (float): diameter of the wheel in mm + tire_size (float): size of tire in mm + chainring (int): teeth of the front gears + cog (int): teeth of the rear gears + cadence (int): cadence in rpm + + Returns: + Tuple: + speed_kmph (float): cycling speed in km/h + speed_mph (float): cycling speed in km/h + ''' + gear_ratio: float = chainring / cog + speed: float = math.pi * (diameter + (2 * tire_size)) \ + * gear_ratio * cadence + speed_kmph: float = speed * 60 / 1_000_000 + speed_mph: float = speed * 60 / 1.609e6 + return speed_kmph, speed_mph +``` + +Notice that the function returns two things, i.e. `speed_kmph` and `speed_mph`. Moreover, we actuall can store these two output into two different variables. + +```python +speed_kmph, speed_mph = calculate_speed(685.8, 38.1, 50, 14, 25) +print(speed_kmph, speed_mph) +``` + +If you just assign it to a single variable, you can still access it using the square bracket and the index. + +```python +speed = calculate_speed(685.8, 38.1, 50, 14, 25) +print(speed[0], speed[1]) +``` + +In the code above, `speed` is of the type `tuple`. You can actually check by using the code below. + +```python +print(type(speed)) +``` + +It will output the following. + +```python + +``` + +In Python, a `tuple` is simply a collection of objects and it is immutable. In our example above, the return value of the function `calculate_speed()` is a collection of two floats. Tuple can consists of objects of different type. The one thing that differentiates it with `list` is that tuple is **immutable**. This means that you cannot modify the element of a tuple. + + +## Creating a Tuple Data Type + +We create a tuple simply by grouping them using a comma. + +```python +>>> my_output: tuple = 12.8, 7.97 +>>> print(my_output) +(12.8, 7.97) +>>> print(type(my_output)) + +``` + +The parenthesis is not a requirement, but often times, it is clearer if you put a parenthesis over the tuple. + +```python +>>> my_output: tuple = (12.8, 7.97) +``` + +We can create a tuple made of objects of different data types as in the example below here. + +```python +>>> various_types_tuple: tuple = 3, 3.14, '3.14', True +>>> print(various_types_tuple) +(3, 3.14, '3.14', True) +``` + +Once we know how to create tuple, now we can discuss on what we can do with tuple data type. + +## Basic Operations with Tuple Data + +The simplest basic operation of tuple data is to access its elements. We access element of a tuple in a similar way as we access a character from a string, i.e. using a square bracket (`[]`) or the get item operator. + +```python +>>> various_types_tuple: tuple = 3, 3.14, '3.14', True +>>> print(various_types_tuple) +(3, 3.14, '3.14', True) +>>> various_types_tuple[0] +3 +>>> various_types_tuple[-1] +True +``` + +In the code above, we access the first and the last element using index 0 and -1 respectively. You can also use slicing operator with tuple similar to string. This can be done using the square bracket operator with colon. + +```python +>>> various_types_tuple +(3, 3.14, '3.14', True) +>>> various_types_tuple[1:] +(3.14, '3.14', True) +>>> various_types_tuple[2:] +('3.14', True) +``` + +In the above code, we slice the tuple from the second element (index 1) to the end and then from the third element (index 2) to the end. You can specify the ending and the step also in the slicing as in the example below. + +```python +>>> various_types_tuple[1:-1:2] +(3.14,) +``` + +In the code above, we start from the second element (index 1) up to but not including the last element (index -1) with a step of 2. Since we only have four elements, the second element is 3.14 and the next element two steps away is already the last element. Since we do not include the last element, the slicing returns only one element in the tuple, i.e. `(3.14,)`. Notice that the output is still a tuple though there is only a single element. A single element tuple has a comma in it without any other element. + +Recall also that tuple is immutable and this is what differentiates a tuple with a list. Immutable simply means that you cannot change the element of a tuple. An exception will be thrown if you try to modify the value of a tuple. + +```python +>>> various_types_tuple[0] = False +Traceback (most recent call last): + File "", line 1, in +TypeError: 'tuple' object does not support item assignment +>>> various_types_tuple +(3, 3.14, '3.14', True) +``` + +In the code above, we tried to modify the value of the first element (index 0) from integer 3 to a boolean `False`. However, Python throws a `TypeError` exception saying that `tuple` object does not support item assignment. Basically it is saying that you cannot modify the element of a tuple. It is immutable. + +Once you create a tuple, you cannot modify it. It has a fixed number of element and it will stay there. You can check the number of element using the `len()` function as usual. + +```python +>>> various_types_tuple +(3, 3.14, '3.14', True) +>>> len(various_types_tuple) +4 +``` + +You can also use `mypy` to check if you accidentally modify a tuple in your code. We have created the file `01_modify_tuple.py` for you to see the output of `mypy` when you try to modify the code. + +```python +# 01_modify_tuple.py +various_types_tuple: tuple = 3, 3.14, '3.14', True +various_types_tuple[0] = False +``` + +Running `mypy` on this file creates the following output. + +```sh +$ mypy 01_modify_tuple.py +01_modify_tuple.py:2: error: Unsupported target for indexed assignment ("tuple[Any, ...]") [index] +Found 1 error in 1 file (checked 1 source file) +``` + +It says that tuple does not suppert indexed assignment and this error occurs at line 2 where we have `various_types_tuple[0] = False`. + +What if you really want to change the element of the tuple? You have two options. The first option is not to use tuple but rather a `list` data type. We will cover list in the next lesson. The second option is to create a new tuple. + +For example, if we want to remove the last element, what we can do is to slice the original tuple creating a new tuple with three elements only. + +```python +>>> various_types_tuple +(3, 3.14, '3.14', True) +>>> new_tuple: tuple = various_types_tuple[:-1] +>>> new_tuple +(3, 3.14, '3.14') +``` + +We can join two tuples using the `+` operator. See example below. + +```python +>>> various_types_tuple +(3, 3.14, '3.14', True) +>>> new_tuple +(3, 3.14, '3.14') +>>> joined_tuple: tuple = various_types_tuple + new_tuple +>>> joined_tuple +(3, 3.14, '3.14', True, 3, 3.14, '3.14') +``` + +We can use this slicing and joining to remove some of the elements in the tuple. For example, let's say we want to remove `True` from the `joined_tuple` variable. We can slice the left up to `True` and joined it with the other tuple starting from after `True`. Notice that `True` is at index 3. We can write the following code. + +```python +>>> removed_bool_tuple: tuple = joined_tuple[:3] + joined_tuple[4:] +>>> removed_bool_tuple +(3, 3.14, '3.14', 3, 3.14, '3.14') +``` + +Similar to string, we can also duplicate the element of a tuple using the `*` operator. + +```python +>>> new_tuple +(3, 3.14, '3.14') +>>> new_tuple * 3 +(3, 3.14, '3.14', 3, 3.14, '3.14', 3, 3.14, '3.14') +``` + +Notice, however, that if we tend to modify the elements of our collection, it is better to use `list` instead of `tuple`. What is then the advantage of tuple over list? +- Tuple, because it is immutable, has a better performance. Iterating over a tuple is faster than iterating over a list. +- Tuple is immutable and so it is safer if you want to have a collection that should not be modified. Programs like `mypy` can help to check your code if you try to modify a tuple. +- Lastly, tuple can be used as a dictionary keys because it is immutable. We will discuss about dictionary in the future lesson. One thing to note is that dictionary keys must be hashable and so it must be immutable. Since list is mutable, it cannot be used as dictionary keys. But we will come to this point again when we dicuss dictionary data type. + +One other basic operations that we can do with a tuple is to check if an element is inside a tuple. We can use the `in` operator which returns `True` or `False`. See the example below. + +```python +>>> new_tuple: tuple = [3, 3.14, '3.14'] +>>> 3 in new_tuple +True +>>> 4 in new_tuple +False +``` + +Recall that `mypy` detects an error when we try to assign to a tuple. We can use type annotation when declaring a tuple with the type of its elements. In the code below, we put all the basic operations with its type annotation when assigning to a new variable. + +```python +various_types_tuple:tuple[int, float, str, bool] = 3, 3.14, '3.14', True +print(various_types_tuple) + +# slicing a tuple +print(various_types_tuple[1:]) +print(various_types_tuple[2:]) +print(various_types_tuple[1:3]) +print(various_types_tuple[1:-1:2]) + +# creating a new tuple using slicing and joining +new_tuple:tuple[int, float, str] = various_types_tuple[:-1] +print(new_tuple) +print(various_types_tuple) +joined_tuple:tuple[int, float, str, bool, int, float, str] = various_types_tuple + new_tuple +print(joined_tuple) +removed_bool_tuple:tuple[int, float, str, int, float, str] = joined_tuple[:3] + joined_tuple[4:] +print(removed_bool_tuple) + +# duplicating a tuple +print(new_tuple * 3) +``` + +When we run `mypy` on this file `02_tuple_basic_operations.py`, we have the following output. + +```sh +$ mypy 02_tuple_basic_operations.py +Success: no issues found in 1 source file +``` + +## Traversing a Tuple + +Tuple is an iterable data type. This means that you can iterate over the elements in a tuple. The simplest way is to use the `for-in` statement as shown below. + +```python +contact:tuple[str, str, str] = ("John", "80043232", "john@mycontact.com") + +for item in contact: + print(item) +``` + +You can run the above code step by step using Python Tutor. + + + +If you need the index as well as the item in the tuple, again, you can use the `enumerate()` function. + +```python +contact:tuple[str, str, str] = ("John", "80043232", "john@mycontact.com") + +for idx, item in enumerate(contact): + print(idx, item) +``` + + + +Similarly, we can also use `while` loop to traverse over the elements of the tuple. However, it is more intuitive and simpler to use for loop in this case. + +## Summary +In this lesson, we introduce the first collection data type called Tuple. A tuple can have a number of elements with different data types inside it. One important aspect about a tuple is that it is immutable. This means that you cannot modify its element once it is created. We will discuss about List data type which is mutable in the next lesson. We showed some basic operations with tuple and how to traverse a tuple using the for-in statement. + + + diff --git a/_Notes/Visualization.md b/_Notes/Visualization.md deleted file mode 100644 index 9154e24..0000000 --- a/_Notes/Visualization.md +++ /dev/null @@ -1,779 +0,0 @@ ---- -title: Visualization -permalink: /notes/visualization -key: notes-visualization -layout: article -nav_key: Notes -sidebar: - nav: Notes -license: false -aside: - toc: true -show_edit_on_github: false -show_date: false ---- - -In this lesson, we will discuss common plots to visualize data using Matplotlib and Seaborn. Seaborn works on top of Matplotlib and you will need to import both packages in most of the cases. - -Reference: -- [Seaborn Tutorial](https://seaborn.pydata.org/tutorial.html) -- [Matplotlib Tutorial](https://matplotlib.org/stable/tutorials/index.html) - -First, let's import the necessary packages in this notebook. - - -```python -import pandas as pd -import matplotlib.pyplot as plt -import seaborn as sns -import numpy as np -``` - -In this notebook, we will still work with HDB resale price dataset to illustrate some visualization we can use. So let's import the dataset. - - -```python -file_url = 'https://www.dropbox.com/s/jz8ck0obu9u1rng/resale-flat-prices-based-on-registration-date-from-jan-2017-onwards.csv?raw=1' -df = pd.read_csv(file_url) -df -``` - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
monthtownflat_typeblockstreet_namestorey_rangefloor_area_sqmflat_modellease_commence_dateremaining_leaseresale_price
02017-01ANG MO KIO2 ROOM406ANG MO KIO AVE 1010 TO 1244.0Improved197961 years 04 months232000.0
12017-01ANG MO KIO3 ROOM108ANG MO KIO AVE 401 TO 0367.0New Generation197860 years 07 months250000.0
22017-01ANG MO KIO3 ROOM602ANG MO KIO AVE 501 TO 0367.0New Generation198062 years 05 months262000.0
32017-01ANG MO KIO3 ROOM465ANG MO KIO AVE 1004 TO 0668.0New Generation198062 years 01 month265000.0
42017-01ANG MO KIO3 ROOM601ANG MO KIO AVE 501 TO 0367.0New Generation198062 years 05 months265000.0
....................................
958532021-04YISHUNEXECUTIVE326YISHUN RING RD10 TO 12146.0Maisonette198866 years 04 months650000.0
958542021-04YISHUNEXECUTIVE360YISHUN RING RD04 TO 06146.0Maisonette198866 years 04 months645000.0
958552021-04YISHUNEXECUTIVE326YISHUN RING RD10 TO 12146.0Maisonette198866 years 04 months585000.0
958562021-04YISHUNEXECUTIVE355YISHUN RING RD10 TO 12146.0Maisonette198866 years 08 months675000.0
958572021-04YISHUNEXECUTIVE277YISHUN ST 2204 TO 06146.0Maisonette198563 years 05 months625000.0
-

95858 rows ร— 11 columns

-
- - - -## Categories of Plots - -There are different categories of plot in Seaborn packages as shown in Seaborn documentation. - -![](https://seaborn.pydata.org/_images/function_overview_8_0.png) - -We can use either scatterplot or lineplot if we want to see relationship between two or more data. On the other hand, we have a few options to see distribution of data. The common one would be a histogram. The last category is categorical plot. We can use box plot, for example, to see the statistics of different categories. We will illustrate this more in the following sections. - -## Histogram and Boxplot - -One of the first thing we may want to do in understanding the data is to see its distribution and its descriptive statistics. To do this, we can use `histplot` to show the histogram of the data and `boxplot` to show the five-number summary of the data. - -Let's see the resale price in the area around Tampines. First, let's check what are the town listed in this data set. - - -```python -np.unique(df['town']) -``` - - - - - array(['ANG MO KIO', 'BEDOK', 'BISHAN', 'BUKIT BATOK', 'BUKIT MERAH', - 'BUKIT PANJANG', 'BUKIT TIMAH', 'CENTRAL AREA', 'CHOA CHU KANG', - 'CLEMENTI', 'GEYLANG', 'HOUGANG', 'JURONG EAST', 'JURONG WEST', - 'KALLANG/WHAMPOA', 'MARINE PARADE', 'PASIR RIS', 'PUNGGOL', - 'QUEENSTOWN', 'SEMBAWANG', 'SENGKANG', 'SERANGOON', 'TAMPINES', - 'TOA PAYOH', 'WOODLANDS', 'YISHUN'], dtype=object) - - - -Now, let's get the data for resale in Tampines only. - - -```python -df_tampines = df.loc[df['town'] == 'TAMPINES',:] -df_tampines -``` - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
monthtownflat_typeblockstreet_namestorey_rangefloor_area_sqmflat_modellease_commence_dateremaining_leaseresale_price
9172017-01TAMPINES2 ROOM299ATAMPINES ST 2201 TO 0345.0Model A201294 years 02 months250000.0
9182017-01TAMPINES3 ROOM403TAMPINES ST 4101 TO 0360.0Improved198567 years 09 months270000.0
9192017-01TAMPINES3 ROOM802TAMPINES AVE 404 TO 0668.0New Generation198466 years 05 months295000.0
9202017-01TAMPINES3 ROOM410TAMPINES ST 4101 TO 0369.0Improved198567 years 08 months300000.0
9212017-01TAMPINES3 ROOM462TAMPINES ST 4407 TO 0964.0Simplified198769 years 06 months305000.0
....................................
956712021-04TAMPINESEXECUTIVE495ETAMPINES ST 4304 TO 06147.0Apartment199471 years 10 months630000.0
956722021-04TAMPINESEXECUTIVE477TAMPINES ST 4304 TO 06153.0Apartment199371 years 04 months780000.0
956732021-04TAMPINESEXECUTIVE497JTAMPINES ST 4510 TO 12139.0Premium Apartment199674 years 03 months695000.0
956742021-04TAMPINESEXECUTIVE857TAMPINES ST 8301 TO 03154.0Maisonette198866 years735000.0
956752021-04TAMPINESMULTI-GENERATION454TAMPINES ST 4201 TO 03132.0Multi Generation198765 years 04 months600000.0
-

6392 rows ร— 11 columns

-
- - - -Now, we can plot its resale price distribution using `histplot`. - -See [documentation for histplot](https://seaborn.pydata.org/generated/seaborn.histplot.html) - - -```python -sns.histplot(x='resale_price', data=df_tampines) -``` - - - - - - - - - -![png](/assets/images/week8/Visualization_11_1.jpeg) - - -In the above plot, we use `df_tampines` as our data source and use `resale_price` column as our x-axis. We can change the plot if we want to show it vertically. - - -```python -sns.set() -sns.histplot(y='resale_price', data=df_tampines) -``` - - - - - - - - - -![png](/assets/images/week8/Visualization_13_1.png) - - -**Notice that the background changes**. This is because we have called `sns.set()` which set Seaborn default setting instead of using Matplotlib's setting. For example, Matplotlib uses whitebackground and no grid. Seaborn by default displays some white grid on gray background. - -By default, the `bins` argument is `auto` and Seaborn will try to calculate how many bins should be used. But we can specify this manually. - - -```python -sns.histplot(y='resale_price', data=df_tampines, bins=10) -``` - - - - - - - - - -![png](/assets/images/week8/Visualization_15_1.png) - - -We can see that majority of the sales of resale HDB in Tampines is priced at about \$400k to \$500k. - -We can also use the `boxplot` to see some descriptive statistics of the data. - -See [documentation on boxplot](https://seaborn.pydata.org/generated/seaborn.boxplot.html) - - -```python -sns.boxplot(x='resale_price', data=df_tampines) -``` - - - - - - - - - -![png](/assets/images/week8/Visualization_17_1.png) - - -See [Understanding Boxplot](https://towardsdatascience.com/understanding-boxplots-5e2df7bcbd51) for more detail. But the figure in that website summarizes the different information given in a boxplot. - -![](https://miro.medium.com/max/700/1*2c21SkzJMf3frPXPAR_gZA.png) - -The box gives you the 25th percentile and the 75th percentile boundary. The line inside the box gives you the median of the data. As we can see the median is about \$400k to \$500k. The difference between the 75th percentile (Q3) and the 25th percentile (Q1) is called the *Interquartile Range* (IQR). This definition is needed to understand what defines **outliers**. The minimum and the maximum here is not the minimum and the maximum value in the data, but rather is capped at - -$$min = Q1 - 1.5\times IQR$$ -$$max = Q3 + 1.5\times IQR$$ - -Anything below or above these "minimum" and "maximum" are considered an outlier in the box plot. In the figure above, for example, we have quite a number of outliers on the high end of the resale price. - -## Modifying Labels and Titles - -Since Seaborn is built on top of Matplotlib, we can use some of Matplotlib functions to change the figure's labels and title. -For example, we can change the histogram's plot x and y labels and its titles using `plt.xlabel()`, `plt.ylabel()`, and `plt.title`. You can access these Matplotlib's functions by first storing the output of your Seaborn plot. - - -```python -myplot = sns.histplot(y='resale_price', data=df_tampines, bins=10) -``` - - -![png](/assets/images/week8/Visualization_22_0.png) - - -Once you obtain the handle, you can call Matplotlib's function by adding the word `set_` in front of it. For example, if the Matplotlib's function is `plt.xlabel()`, you call it as `myplot.set_xlabel()`. See the code below. - - -```python -myplot = sns.histplot(y='resale_price', data=df_tampines, bins=10) -myplot.set_xlabel('Count', fontsize=16) -myplot.set_ylabel('Resale Price', fontsize=16) -myplot.set_title('HDB Resale Price in Tampines', fontsize=16) -``` - - - - - Text(0.5, 1.0, 'HDB Resale Price in Tampines') - - - - -![png](/assets/images/week8/Visualization_24_1.png) - - -Notice now that the plot has a title and both the x and y label has changed. - -The above plot will be much easier if we plots in thousands of dollars. So let's create a new column of resale price in \$1000. - - -```python -df_tampines['resale_price_1000'] = df_tampines['resale_price'].apply(lambda price: price/1000) -df_tampines['resale_price_1000'].describe() -``` - - :1: SettingWithCopyWarning: - A value is trying to be set on a copy of a slice from a DataFrame. - Try using .loc[row_indexer,col_indexer] = value instead - - See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy - df_tampines['resale_price_1000'] = df_tampines['resale_price'].apply(lambda price: price/1000) - - - - - - count 6392.000000 - mean 479.670371 - std 125.569977 - min 238.000000 - 25% 390.000000 - 50% 455.000000 - 75% 550.000000 - max 990.000000 - Name: resale_price_1000, dtype: float64 - - - -Now, let's plot it one more time. - - -```python -myplot = sns.histplot(y='resale_price_1000', data=df_tampines, bins=10) -myplot.set_xlabel('Count', fontsize=16) -myplot.set_ylabel('Resale Price in $1000', fontsize=16) -myplot.set_title('HDB Resale Price in Tampines', fontsize=16) -``` - - - - - Text(0.5, 1.0, 'HDB Resale Price in Tampines') - - - - -![png](/assets/images/week8/Visualization_29_1.png) - - -## Using Hue - -Seaborn make it easy to plot the same data and colour those data depending on another data. For example, we can see the distribution of the resale price according to the room number or the storey range. Seaborn has an argument called `hue` to specify which data column you want to use to colour this. - - -```python -myplot = sns.histplot(y='resale_price_1000', hue='flat_type', data=df_tampines, bins=10) -myplot.set_xlabel('Count', fontsize=16) -myplot.set_ylabel('Resale Price in $1000', fontsize=16) -myplot.set_title('HDB Resale Price in Tampines', fontsize=16) -``` - - - - - Text(0.5, 1.0, 'HDB Resale Price in Tampines') - - - - -![png](/assets/images/week8/Visualization_32_1.png) - - -So we can see from the distribution that 4-room flats in Tampines contributes roughly the largest sales. We can also see that 4-room flat resale price is around the median of the all the resale flats in this area. - - -```python -myplot = sns.histplot(y='resale_price_1000', hue='storey_range', data=df_tampines, bins=10) -myplot.set_xlabel('Count', fontsize=16) -myplot.set_ylabel('Resale Price in $1000', fontsize=16) -myplot.set_title('HDB Resale Price in Tampines', fontsize=16) -``` - - - - - Text(0.5, 1.0, 'HDB Resale Price in Tampines') - - - - -![png](/assets/images/week8/Visualization_34_1.png) - - -The above colouring is not so obvious because they are on top of one another, one way is to change the settings in such a way that it is stacked. We can do this by setting the `multiple` argument for the case when there are multiple data in the same area. - - -```python -myplot = sns.histplot(y='resale_price_1000', hue='storey_range', - multiple='stack', - data=df_tampines, bins=10) -myplot.set_xlabel('Count', fontsize=16) -myplot.set_ylabel('Resale Price in $1000', fontsize=16) -myplot.set_title('HDB Resale Price in Tampines', fontsize=16) -``` - - - - - Text(0.5, 1.0, 'HDB Resale Price in Tampines') - - - - -![png](/assets/images/week8/Visualization_36_1.png) - - -## Scatter Plot and Line Plot - -We use scatter plot and line plot to visualize relationship between two or more data. For example, we can plot the floor area and resale price to see if there is any relationship. - - -```python -myplot = sns.scatterplot(x='floor_area_sqm', y='resale_price_1000', data=df_tampines) -myplot.set_xlabel('Floor Area ($m^2$)') -myplot.set_ylabel('Resale Price in $1000') -``` - - - - - Text(0, 0.5, 'Resale Price in $1000') - - - - -![png](/assets/images/week8/Visualization_38_1.png) - - -As we can see from the plot above, that the price tend to increase with the increase in floor area. You can again use the `hue` argument to see any category in the plot. - - -```python -myplot = sns.scatterplot(x='floor_area_sqm', y='resale_price_1000', - hue='flat_type', - data=df_tampines) -myplot.set_xlabel('Floor Area ($m^2$)') -myplot.set_ylabel('Resale Price in $1000') -``` - - - - - Text(0, 0.5, 'Resale Price in $1000') - - - - -![png](/assets/images/week8/Visualization_40_1.png) - - -We can see that flat type in a way also has relationship with the floor area. - -## Pair Plot - -One useful plot is called Pair Plot in Seaborn where it plots the relationship on multiple data columns. - - -```python -myplot = sns.pairplot(data=df_tampines) -``` - - -![png](/assets/images/week8/Visualization_43_0.png) - - -The above plots immediately plot different scatter plots and histogram in a matrix form. The diagonal of the plot shows the histogram of that column data. The rest of the cell shows you the scatter plot of two columns in the data frame. From these, we can quickly see the relationship between different columns in the data frame. diff --git a/_Notes/While_Loop.md b/_Notes/While_Loop.md new file mode 100644 index 0000000..3fb2b61 --- /dev/null +++ b/_Notes/While_Loop.md @@ -0,0 +1,391 @@ +--- +title: While Loop +permalink: /notes/while-loop +key: notes-while-loop +layout: article +nav_key: Notes +sidebar: + nav: Notes +license: false +aside: + toc: true +show_edit_on_github: false +show_date: false +--- + +## Implementing Iterative Structure using While Loop + +In the previous lesson, we have implemented iterative structure using `for-in` statement. The main criteria in using this statement is that we must have an *iterable* to iterate with. There are times, however, when we do not have an iterable data and yet we want to have iterative structure to repeat. A simple example is a login page where the user name and password prompt is repeatedly presented to the users until the user enters the correct username and password. In this case, it is more natural to use the `while` statement instead of the for-in statement. + +In this lesson, we will first repeat what we do with iterables using for-in statement. However, this time, we will use the `while` statement. This is to highlight the similarity and the differences. After this, we will focus on some cases where it is more natural to use the `while` statement to implement the iterative structure. + +## Basic Structure of a While Loop + +Let's start with the basic syntax for a `while` statement. + +```python +# initialization for condition +# while condition +while condition: + # block A + # which is the code to repeat + # ... + + # block B + # which is to modify the condition +``` + +The syntax for the while-loop has four main parts: +1. The first one is the code to initialize the condition of the while loop. This is needed so that the next code does not throw an error when it checks the condition. +1. The next one is the while statement that contains the condition. As long as the condition is true, block A and block B will be executed. When the condition is no longer true, it will exit the loop and move on to the next code after the while-loop. +1. The third part is block A code which contains code that will be repeated. In the example of a login page, this would be to display the username and password prompt and capturing user input to these data. +1. The last part, which is very important, is block B, which has to be present to modify the state of the condition. If the condition never changes, the boolean evaluation will remain true and the loop never stops. This is what we call as infinite loop. Block B code is the code that makes sure the condition will become false at some points when the iteration should stop. + +The flowchart is shown below. + + + + +Let's take a look at some example. First, let's redo some of the iterative structure in the previous lesson but this time using the `while` statement. The first code we had previously is like the one below here. + +```python +name:str = "John Wick" +for char in name: + print(char) +``` + +How can we write this code using `while` statement. First, we need the code for initialzing the condition and think what that condition would be. We can use a variable to index each of the character in name and we can stop the iteration when the index exceed the last character in the string. + +```python +name: str = "John Wick" +# code to initialize for the while loop condition +# we will use index to access the character in a string +idx: int = 0 + +# while loop statement with condition +while idx < len(name): + # block A + char: str = name[idx] + print(char) + + # block B + idx += 1 +``` + +In the above code, we have the first part which is the initialization code that is needed for the condition in the while statement. In our case, we chose to use the index which is the position of the character as our condition. When the index has exceed the last character index, then we can stop the iteration. The code `len(name)` gives us the length of the string, which in this case is 9 characters. Since the index starts from 0, we will repeat block A and block B as long as the index is less than 9. The index of the last character is at index 8. + +Block A is the code that is repeated as long as the condition is `true`. In our case, we just want to print the character of the string `name`. We do that by first accessing the character using the *get item* operator, i.e. square bracket, and providing its index. Once we have the character, we can use the `print()` function to print it. We repeat these code as long as the index is less than 9. + +Block B is the code that is also repeated as long as the condition of the while statement is `true`. However, the purpose of this code is different. The purpose is to modify the condition so that at some point, the condition will be evaluated as `false`. This is to ensure that the loop **terminates**. In this case, our block B is simply to increase the index by one. Since the indices increases, at some point, it will be equal to 9. This means that the condition `idx < len(name)` will no longer be `true`. At this point, the loop terminates. + +We can see the same structure for various code when using the `while` statement. Let's take another look of our previous code and try to create the code using `while` statement. + +```python +for item in range(10,100,10): + print(item) +``` + +How would we write the code above using `while` statement? Let's start with thinking about the condition of the `while` statement. We can continue printing as long as `item` is less than `100`. What is the initial value for `item`? The `range()` function tells us it starts from `10` and it increases by `10`. Let's assemble the code. + +```python +# code to initialize +item: int = 10 + +# while statement +while item < 100: + # block A + print(item) + + # block B + item += 10 +``` + +In this case, our initialization is simply to set the variable `item` to 10. The condition is as long as `item` is less than 100, we will do two things. The first one is to print the item and the second one is to increase the item by 10. Note that this increment by 10 is part of block B because it changes the condition at every iteration. At some point, the condition will be evaluated as `false`. You can follow the code step by step using Python Tutor below. + + + +## When to use While Loop Instead of For Loop + +In the previous examples, we implemented iterative structure using `while` statement which we convert from the `for-in` statement. The `while` statement is general enough that it can do what `for-in` statement can do. When dealing with *iterable* data types, it is easier and more natural to use `for-in` statement as we iterate for every item in the iterable or in the collection. The `while` statement is used more naturally when there is no obvious iterable data or collection data that we work on. It is also more natural to use the `while` statement when we do not know beforehand how many iteration is needed. An example of this is when users login unsuccessfuly. In this case, the user is presented with the login page repeatedly until he or she enters the right password. Let's write a simple Python code to test this. + +```python +username: str = 'user1' +password: str = 'password4user1' +# code to initialize +username_inp: str = input("Username: ") +password_inp: str = input("Password: ") + +# while loop with condition +while (username_inp != username) or (password_inp != password): + # block A + print("You have entered a wrong username or password. Please try again.") + + # block B + username_inp = input("Username: ") + password_inp = input("Password: ") + +print("You have successfuly login!") + +``` + +The output below show an example when you enter the wrong username or password and then the right one. + +```sh +$ python 01_login.py +Username: d +Password: s +You have entered a wrong username or password. Please try again. +Username: user1 +Password: password4user1 +You have successfuly login! +``` + +Notice that we have put some comments on the above code to show that we still have a similar structure for while loop. The first part is to initialize the variable that will be used as the condition for the while-loop. The second part is the while condition itself inside the `while` statement. The third part is the block A which is the code to be repeated. In this case, we simply print the error message. Lastly, the fourth part is the block B which is the code that modifies the state of the condition. This is needed to ensure that the condition can terminate. If we do not prompt the user to enter the new user name or password, the evaluated condition will not change and the loop will not terminate. + +In the case above, it is more natural to use the `while` statement instead of the `for-in` statement. The reason is that `for-in` statement requires iterable on the right hand side of the `in` keyword. However, in the example above, there is no obvious iterable that we work on. + +Another example for iterative structure that best implemented using the `while` statement is for **loop with sentinel value**. Sentinel value refers to some value that indicate to the program that it is time to end the iteration. In other words, it is the value used as a condition for termination. An example of a sentinel value could be the End Of File character or EOF. The software, let's say a word processor, can continue reading the characters and display them until it reaches the end of file character. + +In the example below, we will show you another simple example in playing games where using while loops is more intuitive as compared to for loop. For example, you have a chatbot that can play guessing game with you. At the end of the game, the chatbot will ask if you want to play another game. If you say yes, it will generate a new guessing game. If you say no, however, it will end the game playing. We can have something like this. + +```python +continue_playing: bool = True +while continue_playing: + play_guessing_game() + continue_playing = ask_user_to_continue() +``` + +In the above code, we have a function call `play_guessing_game()` which may be defined somewhere else. That function will start the guessing game. After the game ends, it will return to this while loop and continue to call `ask_user_to_continue()` function. This function should return either `True` or `False`. If the return value is `False`, the loop ends. However, if the return value is `True`, it will go into the next iteration and call `play_guessing_game()` again. + +Notice that even in the above code, we have the similar structure. First, we initialize `continue_playing` to `True`. Second, we have the while statement that makes use of this variable `continue_playing`. Third, we have the code to be repeated which is a function call to `play_guessing_game()`. Lastly, we have the code that modify `continue_playing` to ensure potential termination of the loop. + + +## Premature Loop Termination +In both while loop and for loop, Python allows you to terminate the loop early using the `break` statement. This is useful when you want to terminate the loop immediately without executing the rest of the code. A simple example would be a program for the robot to continue scanning until it finds the object that he is searching. + +```python +while True: + found = find_object() + if found: + break + move_to_next_location() +``` + +In the above code, we have an infinite loop since the condition is always `True`. The code that is repeated contains two function calls. The first function call is `find_object()`. The second one is `move_to_next_locatioN()`. In our code, when the `find_object()` function returns `True`, it will terminate the loop without executing `move_to_next_location()`. + +The `break` statement allows you to terminate the loop even if the program counter has not reach back to the `while` condition checking. This also allows the code to have multiple conditions at various places in the body of the loop where it can terminate the loop. Without break, the program must reach the `while` condition before it can terminate the loop. Moreover, all the conditions of the termination has to be placed in that `while` condition. + +The `break` statement can be used also inside a for-loop. A simple example is when you want to search for a particular element in a list or a particular character in a string. + +## Using Print to Debug While Loop + +Let's end this section with a simple example where we can apply our problem solving and debugging skills. Let's say, we have to write a code to read a long string and display all the character positions of a given search keywords. For example, let's say we have the following passage which is taken from Sherlock Holmes' "The Adventure of the Speckled Band". + +```python +"Violence does, in truth, recoil upon the violent, and the schemer falls into the pit which he digs for another. Let us thrust this creature back into its den, and we can then remove Miss Stoner to some place of shelter and let the county police know what has happened." +``` + +And the code is to display the position of all the commas in the first sentence. The sentence is ended with a full stop in that passage. Let's write down our **P**roblem Statement. + +``` +Input => Some passage: string +Output => None +Process => display into the screen all the position of the commas in the first sentence. +``` + +We can then now do our **C**oncrete Cases. Using the above passage as the input, we find the following: +- a comma after "does" +- a comma after "truth" +- a comma after "violent" +- a fullstop after "another" + +Once we found a fullstop, we can stop our search. + +Counting from left to right, we can output the following display. + +``` +A comma at position 14. +A comma at position 24. +A comma at position 49. +``` + +Notice that we count our position from 1 instead of 0 in the above output. Moreover, the way we get the position is to point to the character one by one. Let's call this position pointer as our *arrow*. We start with the first character and put our arrow to point to position 1. We can then check if that character is a comma or not. If it is not, we move to the next character and increase our arrow's position. We repeat the check whether it is a comma or not until we find a fullstop. When it is a fullstop, we can exit and terminate the code. + +Now, we can write our **D**esign of Algorithm. You may have noticed in the previous paragraph some iterative structure when we say: + We repeat the check whether it is a comma or not until we find a fullstop. + +We can also spot the branch structure when we hear "check" or "if" or "whether". In the above paragraph, we found the following: + We can then check if that character is a comma or not. + +Let's write down our first draft of the steps. + +``` +1. put arrow to point to the first character +2. check if the character pointed by the arrow is a comma or not. +3. if it is a comma, then + 3.1 print the position +4. move the arrow to point to the next character +5. repeat steps 2 to 4 until we find the fullstop +``` + +That's roughly the steps. Now, we can refine our **D**esign of algorithm in a few ways. +- We can make more specific what it means to point to the first character. Since Python's indexing starts from 0, the first character is at index 0. +- We can move the condition to repeat to the top instead at step 5. The reason is that Python do not have do-while or repeat-until statement. Python only has while-loop. +- Printing the position is not accurate because Python index starts from 0 while we count our position from 1. So we need to add by one when printing the position. + +Let's rewrite our algorithm. + +``` +1. initialize arrow to index 0. +2. initialize a boolean variable found_fullstop to be False. +3. as long as we have not found a fullstop + 3.1 check if the character pointed by the arrow is a comma or not. + 3.1.1 if it is a comma, display the index added by one. + 3.2 Check if the character is a fullstop + 3.2.1 if it is a full stop, set found_fullstop to be True. + 3.3 add arrow position by one. +``` + +We have modified our algorithm in a few significant ways. First we use index 0 to assign to arrow. Next, we have a boolean variable as our while condition. We have moved the condition checking to step 3 with `as long as we have not found a fullstop`. Next, we also have added a check if the character is a fullstop or not. This ensures that the while condition will terminate when it encounters a fullstop. + +Let's start our **I**mplement step and **T**esting step together. We will write the code step by step and print the values along the way to check our code. + +```python +data: str = """Violence does, in truth, recoil upon the violent, and the schemer falls into the pit which he digs for another. Let us thrust this creature back into its den, and we can then remove Miss Stoner to some place of shelter and let county police know what has happened.""" + +arrow: int = 0 +# initialize condition +found_fullstop: bool = False +``` + +In the above, we have done the initialize variable `found_fullstop` that would be used in the `while` condition. We have initialized `arrow` as well. You can choose to print the value of the `arrow` but we will skip it here as we know it will be 0. Our next step is to write the while condition. It is important to write a print statement once we write our loop. However, in order to ensure that our loop terminates, we need to write our block B which is the code to ensure loop termination. The question is what should be this code? Since the condition of the while loop termination is whether we find the fullstop or not, we need some code to check if the character is a fullstop, if it is, then we should change the variable to indicate that we have found the full stop. Let's write all these below. + +```python +data: str = """Violence does, in truth, recoil upon the violent, and the schemer falls into the pit which he digs for another. Let us thrust this creature back into its den, and we can then remove Miss Stoner to some place of shelter and let county police know what has happened.""" + +arrow: int = 0 +# initialize condition +found_fullstop: bool = False +# while statement +while not found_fullstop: + # block A + char: str = data[arrow] + print(char) + # block B + if char == '.': + found_fullstop = True +``` + +You can run the above code using Python Tutor and it will display the first sentence with each character on each line. + + + +However, the program actually displays "V" and it enters into an infinite loop. What happens? As you can observe, "V" is the first character and it seems that the program never moves to the next character. The reason is that we have not implemented step 3.3 to increase the arrow position. Let's fix that. + +```python +data: str = """Violence does, in truth, recoil upon the violent, and the schemer falls into the pit which he digs for another. Let us thrust this creature back into its den, and we can then remove Miss Stoner to some place of shelter and let county police know what has happened.""" + +arrow: int = 0 +# initialize condition +found_fullstop: bool = False +# while statement +while not found_fullstop: + # block A + char: str = data[arrow] + print(char) + # block B + if char == '.': + found_fullstop = True + arrow += 1 +``` + + + +Now, the code does print the first sentence. So we have created our while-loop structure. Notice, that we use the `print()` function as part of block A just to make sure we have the correct while-loop structure first. Now, we can fill in block A code with our actual algorithm. + +Our next step is to check if the character is a comma or not. If it is, then we will print out the position. As you can see from the "check" keyword, it is a branch structure and we will use *if-statement* to implement this. + +```python +data: str = """Violence does, in truth, recoil upon the violent, and the schemer falls into the pit which he digs for another. Let us thrust this creature back into its den, and we can then remove Miss Stoner to some place of shelter and let county police know what has happened.""" + +arrow: int = 0 +# initialize condition +found_fullstop: bool = False +# while statement +while not found_fullstop: + # block A + char: str = data[arrow] + if char == ',': + print(f"A comma at position {arrow + 1}.") + # block B + if char == '.': + found_fullstop = True + arrow += 1 +``` + +In the above code, we have removed our earlier `print()` function and replaced it with our branch structure using if-statement. Our check if whether `char == ','`. If it is, it will print the sentence "A comma at position X.". Notice that we use string interpolation here and we have added `arrow` by one to get the position. + + +The result is the following output. Which is what we expected from our **C**oncrete Cases above. + +``` +A comma at position 14. +A comma at position 24. +A comma at position 49. +``` + +Try running the code step by step using Python Tutor. + + + + +Running `mypy` and `python` output the following. + +```sh +$ mypy 02_count_comma.py +Success: no issues found in 1 source file +$ python 02_count_comma.py +A comma at position 14. +A comma at position 24. +A comma at position 49. +``` + +Instead of using a boolean variable `found_fullstop`, the code can be written by looping till the end of the string and end prematurely when it sees a fullstop. See an alternative code below. + +```python +data:str = """Violence does, in truth, recoil upon the violent, and the schemer falls into the pit which he digs for another. Let us thrust this creature back into its den, and we can then remove Miss Stoner to some place of shelter and let county police know what has happened.""" + +# initialize condition +arrow:int = 0 + +# while statement +while arrow < len(data): + # block A + char:str = data[arrow] + if char == ',': + print(f"A comma at position {arrow + 1}.") + if char == '.': + break + # block B + arrow += 1 +``` + +In the code above, we no longer have our `found_fullstop` boolean variable. Now, our `while` condition is `while arrow < len(data)`. This also changes our initialization block. Now, `arrow` is considered as part of this initialization condition because `arrow` is in the condition to continue or terminate the loop. The other changes that we have in the last if statement. When we sees a fullstop, it executes `break` statement. As mentioned earlier, the `break` statement terminates the loop immediately without executing the other statements such as the `arrow += 1`. This statement now, is no longer part of block B but rather part of block A. The code in block B is simply the line that increases the arrow position. This will ensure that the condition for the loop terminates when it reaches the end of the string. + + +We can run `mypy` and `python` from the script `03_count_comma.py` as well. + +```sh +$ mypy 03_count_comma.py +Success: no issues found in 1 source file +$ python 03_count_comma.py +A comma at position 14. +A comma at position 24. +A comma at position 49. +``` + +## Summary + +In this lesson, we show another way to implement iterative structure using the `while` statement. We first showed that this while loop can implement all those code that uses for loop in our previous lesson. We discuss when it is more appropriate to use for-loop and when it is more appropriate to use while loop. When we are dealing with iterable data, it is simpler to use for-loop. However, while loop can be used when we do not have a predetermined number of iteration. + +We also discussed about the break statement that can be used to terminate the loop prematurely before the while condition actually terminates the loop. We end the lesson by applying PCDIT framework to a simple problem. We first use a boolean variable as a condition to terminate the loop. However, with the break statement, we can actually use the string data as part of the condition to terminate the loop. In this case, actually, we can make use of for-loop to solve the same problem since string data is an iterable data. You may want to try to re-write the solution using for-loop and a break statement. \ No newline at end of file diff --git a/_Notes/Working_With_Data.md b/_Notes/Working_With_Data.md deleted file mode 100644 index 738d51e..0000000 --- a/_Notes/Working_With_Data.md +++ /dev/null @@ -1,2595 +0,0 @@ ---- -title: Working With Data -permalink: /notes/working_with_data -key: notes-working-with-data -layout: article -nav_key: Notes -sidebar: - nav: Notes -license: false -aside: - toc: true -show_edit_on_github: false -show_date: false ---- - -## Short Introduction to Machine Learning - -In the previous weeks we have discussed how various computation can be done. We begin by discussing computational complexity and divide and conquer strategy through recursion. We then discusses how data can have computation associated with it through object oriented programming. We discussed several data structures and algorithms associated with various problems. We then end up with offering a different perspective by looking at computation as a state machine. - -In this second half of the course, we will look into how computation can learn from data in order to make a new computation. This new computation is often called a **prediction**. In these lessons, we focus on what is called as **supervised machine learning**. The word supervised machine learning indicates that the computer learns from some existing data on how to compute the prediction. An example of this would be given some images labelled as "cat" and "not a cat", the computer can learn to predict (or to compute) whether any a new image given to it is a cat or not a cat. - -cat | not a cat | -:-------------------------:|:-------------------------: -![](https://upload.wikimedia.org/wikipedia/commons/f/ff/Cat_on_laptop_-_Just_Browsing.jpg) | ![](https://upload.wikimedia.org/wikipedia/commons/4/49/Catfish.jpeg) - -Another example would be given a some data of housing prices in Singapore with the year of sale, area, number of rooms, and its floor hight, the computer can predict the price of another house. One other example would be given data of breast cancer cell and its measurements, one can predict whether the cell is malignant or benight. Supervised machine learning assumes that we have some existing data **labelled** with this category "malignant" and "benign". Using this labelled data (supervised), the computer can predict the category given some new data. - -## Reading Data - -The first step in machine learning would be to understand the data itself. In order to do that we need to be able to read data from some source. One common source is a text file in the form of CSV format (comma separated value). Another common format is Excel spreadsheet. The data can be from some databases or some server. Different data sources will require different ways of handling it. But in all those cases we will need to know how to read those data. - -For this purpose we will use [Pandas](https://pandas.pydata.org/) library to read our data. We import the data into our Python code by typing the following code. - -```python -import pandas as pd -``` - -Now we can use Pandas functions to read the data. For example, if we want to read a CSV file, we can simply type: -```python -df = pd.read_csv('mydata.csv') -``` - -Let's take an example of Singapore housing prices. We can get some of these data from [Data.gov.sg](https://data.gov.sg/). We have downloaded the CSV file so that you can access it simply from the following [dropbox link](https://www.dropbox.com/s/jz8ck0obu9u1rng/resale-flat-prices-based-on-registration-date-from-jan-2017-onwards.csv?raw=1). We can use the url to the raw file to open the CSV in our Python code. - - - - -```python -import pandas as pd - -file_url = 'https://www.dropbox.com/s/jz8ck0obu9u1rng/resale-flat-prices-based-on-registration-date-from-jan-2017-onwards.csv?raw=1' -df = pd.read_csv(file_url) -df -``` - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
monthtownflat_typeblockstreet_namestorey_rangefloor_area_sqmflat_modellease_commence_dateremaining_leaseresale_price
02017-01ANG MO KIO2 ROOM406ANG MO KIO AVE 1010 TO 1244.0Improved197961 years 04 months232000.0
12017-01ANG MO KIO3 ROOM108ANG MO KIO AVE 401 TO 0367.0New Generation197860 years 07 months250000.0
22017-01ANG MO KIO3 ROOM602ANG MO KIO AVE 501 TO 0367.0New Generation198062 years 05 months262000.0
32017-01ANG MO KIO3 ROOM465ANG MO KIO AVE 1004 TO 0668.0New Generation198062 years 01 month265000.0
42017-01ANG MO KIO3 ROOM601ANG MO KIO AVE 501 TO 0367.0New Generation198062 years 05 months265000.0
....................................
958532021-04YISHUNEXECUTIVE326YISHUN RING RD10 TO 12146.0Maisonette198866 years 04 months650000.0
958542021-04YISHUNEXECUTIVE360YISHUN RING RD04 TO 06146.0Maisonette198866 years 04 months645000.0
958552021-04YISHUNEXECUTIVE326YISHUN RING RD10 TO 12146.0Maisonette198866 years 04 months585000.0
958562021-04YISHUNEXECUTIVE355YISHUN RING RD10 TO 12146.0Maisonette198866 years 08 months675000.0
958572021-04YISHUNEXECUTIVE277YISHUN ST 2204 TO 06146.0Maisonette198563 years 05 months625000.0
-

95858 rows ร— 11 columns

-
- - - -The output of `read_csv()` function is in Pandas' `DataFrame` type. - - -```python -type(df) -``` - - - - - pandas.core.frame.DataFrame - - - -`DataFrame` is Pandas' class that contains attributes and methods to work with a tabular data as shown above. Recall that we can create our own custom data type using the keyword `class` and define its attributes and methods. We can even override some of Python operators to work with our new data type. This is what Pandas library does with `DataFrame`. This `DataFrame` class provides some properties and methods that allows us to work with tabular data. - -For example, we can get all the name of the columns in our data frame using `df.columns` properties. - - -```python -df.columns -``` - - - - - Index(['month', 'town', 'flat_type', 'block', 'street_name', 'storey_range', - 'floor_area_sqm', 'flat_model', 'lease_commence_date', - 'remaining_lease', 'resale_price'], - dtype='object') - - - -We can also get the index (the names on the rows) using `df.index`. - - -```python -df.index -``` - - - - - RangeIndex(start=0, stop=95858, step=1) - - - -We can also treat this data frame as a kind of matrix to find its shape using `df.shape`. - - -```python -df.shape -``` - - - - - (95858, 11) - - - -As we can see, the data contains 95858 rows and 11 columns. One of the column names is called `resale_price`. Since our aim is to predict the house price, this column is usually called the **target**. The rest of the columns is called the **features**. This means that we have about 10 feature columns. - -The idea supervised machine learning is that using the given data such as shown above, the computer would like to predict what is the *target* give a new set of *features*. The computer does this by *learning* the existing labelled data. The label in this case is the resale price from the historical sales data. - -In order to understand the data, it is important to be able to manipulate and work on the data frame. - -## Data Frame Operations - -It is important to know how to manipulate the data. Pandas has two data structures: -- [Series](https://pandas.pydata.org/docs/user_guide/dsintro.html#series) -- [DataFrame](https://pandas.pydata.org/docs/user_guide/dsintro.html#dataframe) - -You can consider `Series` data as one-dimensional labelled array while `DataFrame` data as two-dimensional labelled data structure. For example, the table that we saw previously, which is the output of `read_csv()` function, is a `DataFrame` because it has both rows and columns and, therefore, two dimensional. On the other hand, we can access just one of the columns from the data frame to get a `Series` data. - -### Getting a Column or a Row as a Series - -You can access the column data as series using the square bracket operator. - -```python -df[column_name] -``` - - -```python -print(df['resale_price']) -print(type(df['resale_price'])) -``` - - 0 232000.0 - 1 250000.0 - 2 262000.0 - 3 265000.0 - 4 265000.0 - ... - 95853 650000.0 - 95854 645000.0 - 95855 585000.0 - 95856 675000.0 - 95857 625000.0 - Name: resale_price, Length: 95858, dtype: float64 - - - -The code above prints the column `resale_price` and its type. As can be seen the type of the output is a `Series` data type. - -You can also get some particular row by specifying its `index`. - -You can also access the column using the `.loc[index, column]` method. In this method, you need to specify the labels of the index. For example, to access all the rows for a particular column called `resale_price`, we can do as follows. Notice that we use `:` to access all the rows. Moreover, we specify the name of the columns in the code below. - - -```python -print(df.loc[:, 'resale_price']) -print(type(df.loc[:, 'resale_price'])) -``` - - 0 232000.0 - 1 250000.0 - 2 262000.0 - 3 265000.0 - 4 265000.0 - ... - 95853 650000.0 - 95854 645000.0 - 95855 585000.0 - 95856 675000.0 - 95857 625000.0 - Name: resale_price, Length: 95858, dtype: float64 - - - -In the above code, we set the index to access all rows by using `:`. Recall that in Python's list slicing, we also use `:` to access all the element. Similarly here, we use `:` to access all the rows. In a similar way, we can use `:` to access all the columns, e.g. `df.loc[:, :]` will copy the whole data frame. - -This also gives you a hint how to access a particular row. Let's say, you only want to acces the first row, you can type the following. - - -```python -print(df.loc[0, :]) -print(type(df.loc[0, :])) -``` - - month 2017-01 - town ANG MO KIO - flat_type 2 ROOM - block 406 - street_name ANG MO KIO AVE 10 - storey_range 10 TO 12 - floor_area_sqm 44 - flat_model Improved - lease_commence_date 1979 - remaining_lease 61 years 04 months - resale_price 232000 - Name: 0, dtype: object - - - -In the above code, we access the first row, which is at index 0, and all the columns. - -Recall that all these data are of the type `Series`. You can create a Data Frame from an existing series just like when you create any other object by instantiating a `DataFrame` object and passing on an argument as shown below. - - -```python -df_row0 = pd.DataFrame(df.loc[0, :]) -df_row0 -``` - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
0
month2017-01
townANG MO KIO
flat_type2 ROOM
block406
street_nameANG MO KIO AVE 10
storey_range10 TO 12
floor_area_sqm44
flat_modelImproved
lease_commence_date1979
remaining_lease61 years 04 months
resale_price232000
-
- - - -### Getting Rows and Columns as DataFrame - -The operator `:` works similar to Python's slicing. This means that you can get some rows by slicing them. For example, you can access the first 10 rows as follows. - - -```python -print(df.loc[0:10, :]) -print(type(df.loc[0:10, :])) -``` - - month town flat_type block street_name storey_range \ - 0 2017-01 ANG MO KIO 2 ROOM 406 ANG MO KIO AVE 10 10 TO 12 - 1 2017-01 ANG MO KIO 3 ROOM 108 ANG MO KIO AVE 4 01 TO 03 - 2 2017-01 ANG MO KIO 3 ROOM 602 ANG MO KIO AVE 5 01 TO 03 - 3 2017-01 ANG MO KIO 3 ROOM 465 ANG MO KIO AVE 10 04 TO 06 - 4 2017-01 ANG MO KIO 3 ROOM 601 ANG MO KIO AVE 5 01 TO 03 - 5 2017-01 ANG MO KIO 3 ROOM 150 ANG MO KIO AVE 5 01 TO 03 - 6 2017-01 ANG MO KIO 3 ROOM 447 ANG MO KIO AVE 10 04 TO 06 - 7 2017-01 ANG MO KIO 3 ROOM 218 ANG MO KIO AVE 1 04 TO 06 - 8 2017-01 ANG MO KIO 3 ROOM 447 ANG MO KIO AVE 10 04 TO 06 - 9 2017-01 ANG MO KIO 3 ROOM 571 ANG MO KIO AVE 3 01 TO 03 - 10 2017-01 ANG MO KIO 3 ROOM 534 ANG MO KIO AVE 10 01 TO 03 - - floor_area_sqm flat_model lease_commence_date remaining_lease \ - 0 44.0 Improved 1979 61 years 04 months - 1 67.0 New Generation 1978 60 years 07 months - 2 67.0 New Generation 1980 62 years 05 months - 3 68.0 New Generation 1980 62 years 01 month - 4 67.0 New Generation 1980 62 years 05 months - 5 68.0 New Generation 1981 63 years - 6 68.0 New Generation 1979 61 years 06 months - 7 67.0 New Generation 1976 58 years 04 months - 8 68.0 New Generation 1979 61 years 06 months - 9 67.0 New Generation 1979 61 years 04 months - 10 68.0 New Generation 1980 62 years 01 month - - resale_price - 0 232000.0 - 1 250000.0 - 2 262000.0 - 3 265000.0 - 4 265000.0 - 5 275000.0 - 6 280000.0 - 7 285000.0 - 8 285000.0 - 9 285000.0 - 10 288500.0 - - - -Notice, however, that the slicing in Pandas' data frame is **inclusive** of the ending index unlike Python's slicing. The other thing to note about is that the output data type is no longer a series but rather a `DataFrame`. The reason is that now the data is two-dimensionsional. - -You can specify both the rows and the columns you want as shown below. - - -```python -df.loc[0:10,'month':'remaining_lease'] -``` - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
monthtownflat_typeblockstreet_namestorey_rangefloor_area_sqmflat_modellease_commence_dateremaining_lease
02017-01ANG MO KIO2 ROOM406ANG MO KIO AVE 1010 TO 1244.0Improved197961 years 04 months
12017-01ANG MO KIO3 ROOM108ANG MO KIO AVE 401 TO 0367.0New Generation197860 years 07 months
22017-01ANG MO KIO3 ROOM602ANG MO KIO AVE 501 TO 0367.0New Generation198062 years 05 months
32017-01ANG MO KIO3 ROOM465ANG MO KIO AVE 1004 TO 0668.0New Generation198062 years 01 month
42017-01ANG MO KIO3 ROOM601ANG MO KIO AVE 501 TO 0367.0New Generation198062 years 05 months
52017-01ANG MO KIO3 ROOM150ANG MO KIO AVE 501 TO 0368.0New Generation198163 years
62017-01ANG MO KIO3 ROOM447ANG MO KIO AVE 1004 TO 0668.0New Generation197961 years 06 months
72017-01ANG MO KIO3 ROOM218ANG MO KIO AVE 104 TO 0667.0New Generation197658 years 04 months
82017-01ANG MO KIO3 ROOM447ANG MO KIO AVE 1004 TO 0668.0New Generation197961 years 06 months
92017-01ANG MO KIO3 ROOM571ANG MO KIO AVE 301 TO 0367.0New Generation197961 years 04 months
102017-01ANG MO KIO3 ROOM534ANG MO KIO AVE 1001 TO 0368.0New Generation198062 years 01 month
-
- - - -If you want to select the column, you can pass on a list of columns as shown in the example below. - - -```python -columns = ['town', 'block', 'resale_price'] -df.loc[:, columns] -``` - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
townblockresale_price
0ANG MO KIO406232000.0
1ANG MO KIO108250000.0
2ANG MO KIO602262000.0
3ANG MO KIO465265000.0
4ANG MO KIO601265000.0
............
95853YISHUN326650000.0
95854YISHUN360645000.0
95855YISHUN326585000.0
95856YISHUN355675000.0
95857YISHUN277625000.0
-

95858 rows ร— 3 columns

-
- - - -A similar output can be obtained without `.loc` - - -```python -df[columns] -``` - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
townblockresale_price
0ANG MO KIO406232000.0
1ANG MO KIO108250000.0
2ANG MO KIO602262000.0
3ANG MO KIO465265000.0
4ANG MO KIO601265000.0
............
95853YISHUN326650000.0
95854YISHUN360645000.0
95855YISHUN326585000.0
95856YISHUN355675000.0
95857YISHUN277625000.0
-

95858 rows ร— 3 columns

-
- - - -You can also combine specifying the rows and the columns as usual using `.loc`. - - -```python -df.loc[0:10, columns] -``` - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
townblockresale_price
0ANG MO KIO406232000.0
1ANG MO KIO108250000.0
2ANG MO KIO602262000.0
3ANG MO KIO465265000.0
4ANG MO KIO601265000.0
5ANG MO KIO150275000.0
6ANG MO KIO447280000.0
7ANG MO KIO218285000.0
8ANG MO KIO447285000.0
9ANG MO KIO571285000.0
10ANG MO KIO534288500.0
-
- - - -The index is not always necessarily be an integer. Pandas can take strings as the index of a data frame. But there are times, even when the index is not an integer, we still prefer to locate using the position of the rows to select. In this case, we can use `.iloc[position_index, position_column]`. - - -```python -columns = [1, 3, -1] -df.iloc[0:10, columns] -``` - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
townblockresale_price
0ANG MO KIO406232000.0
1ANG MO KIO108250000.0
2ANG MO KIO602262000.0
3ANG MO KIO465265000.0
4ANG MO KIO601265000.0
5ANG MO KIO150275000.0
6ANG MO KIO447280000.0
7ANG MO KIO218285000.0
8ANG MO KIO447285000.0
9ANG MO KIO571285000.0
-
- - - -The above code gives the same data frame but it uses different input to specifies. By using `.iloc[]`, we specify the position of the index and the columns instead of the label of the index and the columns. It happens that for the index, the position numbering is exactly the same as the label. - -### Selecting Data Using Conditions - -We can use conditions with Pandas' data frame to select particular rows and columns using either `.loc[]` or `.iloc[]`. The reason is that these methods can take in boolean arrays. - -Let's see some examples below. First, let's list down the resale price by focusing on the block at a given town. - - - - -```python -columns = ['town', 'block', 'resale_price'] -df.loc[:, columns] -``` - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
townblockresale_price
0ANG MO KIO406232000.0
1ANG MO KIO108250000.0
2ANG MO KIO602262000.0
3ANG MO KIO465265000.0
4ANG MO KIO601265000.0
............
95853YISHUN326650000.0
95854YISHUN360645000.0
95855YISHUN326585000.0
95856YISHUN355675000.0
95857YISHUN277625000.0
-

95858 rows ร— 3 columns

-
- - - -Let's say we want to see those sales where the price is greater than \$500k. We can put in this condition in filtering the rows. - - -```python -df.loc[df['resale_price'] > 500_000, columns] -``` - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
townblockresale_price
43ANG MO KIO304518000.0
44ANG MO KIO646518000.0
45ANG MO KIO328560000.0
46ANG MO KIO588C688000.0
47ANG MO KIO588D730000.0
............
95853YISHUN326650000.0
95854YISHUN360645000.0
95855YISHUN326585000.0
95856YISHUN355675000.0
95857YISHUN277625000.0
-

27233 rows ร— 3 columns

-
- - - -Note: Python ignores the underscores in between numeric literals and you can use it to make it easier to read. - -Let's say if we want to find all those sales between \\$500k and \\$600k only, we can use the AND operator `&` to have more than one conditions. - - -```python -df.loc[(df['resale_price'] >= 500_000) & (df['resale_price'] <= 600_000), columns] -``` - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
townblockresale_price
43ANG MO KIO304518000.0
44ANG MO KIO646518000.0
45ANG MO KIO328560000.0
49ANG MO KIO101500000.0
110BEDOK185580000.0
............
95849YISHUN504C550000.0
95850YISHUN511B600000.0
95851YISHUN504C590000.0
95852YISHUN838571888.0
95855YISHUN326585000.0
-

13478 rows ร— 3 columns

-
- - - -**Note: the parenthesis separating the two AND conditions are compulsory.** - -We can also specify more conditions. For example, we are only interested in ANG MO KIO area. We can have the following code. - - -```python -df.loc[(df['resale_price'] >= 500_000) & (df['resale_price'] <= 600_000) & - (df['town'] == 'ANG MO KIO'), columns] -``` - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
townblockresale_price
43ANG MO KIO304518000.0
44ANG MO KIO646518000.0
45ANG MO KIO328560000.0
49ANG MO KIO101500000.0
1219ANG MO KIO351530000.0
............
94741ANG MO KIO545590000.0
94742ANG MO KIO545600000.0
94743ANG MO KIO551520000.0
94746ANG MO KIO642545000.0
94749ANG MO KIO353588000.0
-

329 rows ร— 3 columns

-
- - - -If you are interested only in blocks 300s and 400s, you can add this conditions further. - - -```python -df.loc[(df['resale_price'] >= 500_000) & (df['resale_price'] <= 600_000) & - (df['town'] == 'ANG MO KIO') & - (df['block'] >= '300') & (df['block'] < '500'), columns] -``` - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
townblockresale_price
43ANG MO KIO304518000.0
45ANG MO KIO328560000.0
1219ANG MO KIO351530000.0
2337ANG MO KIO344538000.0
2338ANG MO KIO329548000.0
............
92327ANG MO KIO353520000.0
92332ANG MO KIO459600000.0
92335ANG MO KIO459500000.0
94740ANG MO KIO305537000.0
94749ANG MO KIO353588000.0
-

174 rows ร— 3 columns

-
- - - -## Series and DataFrame Functions - -Pandas also provides several functions that can be useful in understanding the data. In this section, we will explore some of these. - - - -### Creating DataFrame and Series - -We can create a new DataFrame from other data type such as dictionary, list-like objects, or Series. For example, given a `Series`, you can convert into a `DataFrame` as shown below. - - -```python -price = df['resale_price'] -print(isinstance(price, pd.Series)) -price_df = pd.DataFrame(price) -print(isinstance(price_df, pd.DataFrame)) -price_df -``` - - True - True - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
resale_price
0232000.0
1250000.0
2262000.0
3265000.0
4265000.0
......
95853650000.0
95854645000.0
95855585000.0
95856675000.0
95857625000.0
-

95858 rows ร— 1 columns

-
- - - -Similarly, you can convert other data to a `Series` by using its contructor. In the example below, we create a new series from a list of integers from 2 to 100. - - -```python -new_series = pd.Series(list(range(2,101))) -print(isinstance(new_series, pd.Series)) -new_series -``` - - True - - - - - - 0 2 - 1 3 - 2 4 - 3 5 - 4 6 - ... - 94 96 - 95 97 - 96 98 - 97 99 - 98 100 - Length: 99, dtype: int64 - - - -### Copying - -One useful function is to copy a data frame to another dataframe. We can use `df.copy()`. This function has an argument `deep` which by default is `True`. If it is true, it will do a deep copy of the Data Frame. Otherwise, it will just do a shallow copy. See [documention](https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.copy.html). - - -```python -df2 = df.copy() -df2 -``` - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
monthtownflat_typeblockstreet_namestorey_rangefloor_area_sqmflat_modellease_commence_dateremaining_leaseresale_price
02017-01ANG MO KIO2 ROOM406ANG MO KIO AVE 1010 TO 1244.0Improved197961 years 04 months232000.0
12017-01ANG MO KIO3 ROOM108ANG MO KIO AVE 401 TO 0367.0New Generation197860 years 07 months250000.0
22017-01ANG MO KIO3 ROOM602ANG MO KIO AVE 501 TO 0367.0New Generation198062 years 05 months262000.0
32017-01ANG MO KIO3 ROOM465ANG MO KIO AVE 1004 TO 0668.0New Generation198062 years 01 month265000.0
42017-01ANG MO KIO3 ROOM601ANG MO KIO AVE 501 TO 0367.0New Generation198062 years 05 months265000.0
....................................
958532021-04YISHUNEXECUTIVE326YISHUN RING RD10 TO 12146.0Maisonette198866 years 04 months650000.0
958542021-04YISHUNEXECUTIVE360YISHUN RING RD04 TO 06146.0Maisonette198866 years 04 months645000.0
958552021-04YISHUNEXECUTIVE326YISHUN RING RD10 TO 12146.0Maisonette198866 years 04 months585000.0
958562021-04YISHUNEXECUTIVE355YISHUN RING RD10 TO 12146.0Maisonette198866 years 08 months675000.0
958572021-04YISHUNEXECUTIVE277YISHUN ST 2204 TO 06146.0Maisonette198563 years 05 months625000.0
-

95858 rows ร— 11 columns

-
- - - -### Statistical Functions - -We can get some descriptive statistics about the data using some of Pandas functions. For example, we can get the five point summary using `.describe()` method. - - -```python -df.describe() -``` - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
floor_area_sqmlease_commence_dateresale_price
count95858.00000095858.0000009.585800e+04
mean97.7722341994.5539344.467242e+05
std24.23879913.1289131.552974e+05
min31.0000001966.0000001.400000e+05
25%82.0000001984.0000003.350000e+05
50%95.0000001995.0000004.160000e+05
75%113.0000002004.0000005.250000e+05
max249.0000002019.0000001.258000e+06
-
- - - -The above code only shows a few columns because the other columns are not numbers. Pandas will only try to get the statistics of the columns that contain numeric numbers. We can also get the individual statistical functions as shown below. - - -```python -print(df['resale_price'].mean()) -``` - - 446724.22886801313 - - - -```python -print(df['resale_price'].std()) -``` - - 155297.43748684428 - - - -```python -print(df['resale_price'].min()) -``` - - 140000.0 - - - -```python -print(df['resale_price'].max()) -``` - - 1258000.0 - - - -```python -print(df['resale_price'].quantile(q=0.75)) -``` - - 525000.0 - - -You can change the way the statistics is computed. Currently, the statistics is calculated over all the rows in the vertical dimension. This is what is considered as `axis=0` in Pandas. You can change it to compute over all the columns by specifying `axis=1`. - - -```python -df.mean(axis=1) -``` - - - - - 0 78007.666667 - 1 84015.000000 - 2 88015.666667 - 3 89016.000000 - 4 89015.666667 - ... - 95853 217378.000000 - 95854 215711.333333 - 95855 195711.333333 - 95856 225711.333333 - 95857 209043.666667 - Length: 95858, dtype: float64 - - - -Again, Pandas only computes the mean from the numeric data across the columns. - -### Transposing Data Frame - -You can also change the rows into the column and the column into the rows. For example, previously we have this data frame we created from a `Series` when extracting row 0. - - -```python -df_row0 -``` - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
0
month2017-01
townANG MO KIO
flat_type2 ROOM
block406
street_nameANG MO KIO AVE 10
storey_range10 TO 12
floor_area_sqm44
flat_modelImproved
lease_commence_date1979
remaining_lease61 years 04 months
resale_price232000
-
- - - -In the above code, the column is row 0 and the rows are the different column names. You can transpose the data using the `.T` property. - - -```python -df_row0_transposed = df_row0.T -df_row0_transposed -``` - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
monthtownflat_typeblockstreet_namestorey_rangefloor_area_sqmflat_modellease_commence_dateremaining_leaseresale_price
02017-01ANG MO KIO2 ROOM406ANG MO KIO AVE 1010 TO 1244Improved197961 years 04 months232000
-
- - - -### Vector Operations - -One useful function in Pandas is `.apply()` (see [documentation](https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.apply.html)) where we can apply some function to all the data in the column or row or Series in a vectorized manner. In this way, we need not iterate or loop the data one at a time to apply this computation. - -For example, if we want to create a column for resale price in terms of $1000, we can use the `.apply()` method by dividing the `resale_price` column with 1000. - - -```python -def divide_by_1000(data): - return data / 1000 - -df['resale_price_in1000'] = df['resale_price'].apply(divide_by_1000) -df['resale_price_in1000'] -``` - - - - - 0 232.0 - 1 250.0 - 2 262.0 - 3 265.0 - 4 265.0 - ... - 95853 650.0 - 95854 645.0 - 95855 585.0 - 95856 675.0 - 95857 625.0 - Name: resale_price_in1000, Length: 95858, dtype: float64 - - - -The method `.apply()` takes in a function that will be processed for every data in that Series. Instead of creating a named function, we can make use of Python's lambda function to do the same. - - -```python -df['resale_price_in1000'] = df['resale_price'].apply(lambda data: data/1000) -df['resale_price_in1000'] -``` - - - - - 0 232.0 - 1 250.0 - 2 262.0 - 3 265.0 - 4 265.0 - ... - 95853 650.0 - 95854 645.0 - 95855 585.0 - 95856 675.0 - 95857 625.0 - Name: resale_price_in1000, Length: 95858, dtype: float64 - - - -Notice that the argument in `divide_by_1000()` becomes the first token after the keyword `lambda`. The return value of the function is provided after the colon, i.e. `:`. - -You can use this to process and create any other kind of data. For example, we can create a new categorical column called "Pricey" and set any sales above \$500k is considered as pricey otherwise is not. If it is pricey, we will label it as 1, otherwise, as 0. - - -```python -df['pricey'] = df['resale_price_in1000'].apply(lambda price: 1 if price > 500 else 0 ) -df[['resale_price_in1000', 'pricey']] -``` - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
resale_price_in1000pricey
0232.00
1250.00
2262.00
3265.00
4265.00
.........
95853650.01
95854645.01
95855585.01
95856675.01
95857625.01
-

95858 rows ร— 2 columns

-
- - - -In the above function, we use the if *expression* to specify the return value for the lambda function. It follows the following format: - -```python -expression_if_true if condition else expression_if_false -``` - -There are many other Pandas functions and methods. It is recommended that you look into the documentation for further references. - -## Reference - -- [Pandas User Guide](https://pandas.pydata.org/docs/user_guide/index.html) -- [Pandas API Reference](https://pandas.pydata.org/docs/reference/index.html) - -## Normalization - -Many times, we will need to normalize the data, both the features and the target. The reason is that each column in the dataset may have different scales. For example, the column `floor_area_sqm` is in between 33 to 249 while `lease_commense_date` is actually in a range between 1966 and 2019. See below statistics for these two columns. - - -```python -display(df['floor_area_sqm'].describe()) -display(df['lease_commence_date'].describe()) -``` - - - count 95858.000000 - mean 97.772234 - std 24.238799 - min 31.000000 - 25% 82.000000 - 50% 95.000000 - 75% 113.000000 - max 249.000000 - Name: floor_area_sqm, dtype: float64 - - - - count 95858.000000 - mean 1994.553934 - std 13.128913 - min 1966.000000 - 25% 1984.000000 - 50% 1995.000000 - 75% 2004.000000 - max 2019.000000 - Name: lease_commence_date, dtype: float64 - - -As we will see later in subsequent weeks, we usually need to normalize the data before doing any training for our machine learning model. There are two common normalization: -- z normalization -- minmax normalization - -You will work on the functions to calculate these normalization in your problem sets. - -### Z Normalization - -This is also called as standardization. In this tranformation, we move the mean of the data distribution to 0 and its standard deviation to 1. The equation is given as follows. - -$$normalized = \frac{data - \mu}{\sigma}$$ - -### Min-Max Normalization - -In this transformation, we scale the data in such a way that the maximum value in the distribution is 1 and its minimum value is 0. We can scale it using the following equation. - -$$normalized = \frac{data - min}{max - min}$$ - -## Splitting Dataset - -One common pre-processing operations that we normally do in machine learning is to split the data into: -- **training** dataset -- **test** dataset - -The idea of splitting the dataset is simply because we should **NOT** use the same data to verify the model which we train. Let's illustrate this using our HDB resale price dataset. We have this HDB resale price dataset with 95858 entries. In our machine learning process, we would like to use this data to do the following: -1. Train the model using the dataset -1. Verify the accuracy of the model - -If we only have one dataset, we cannot use the same data to verify the accuracy with the ones we use to train the model. This bias would obviously create high accuracy. The analogy is like when a teacher giving exactly the same question during the exam as the ones during the practice session. - -To overcome this, we should split the data into two. One set is used to train the model while the other one is used to verify the model. Coming back to the analogy of the teacher giving the exam, if the teacher has a bank of questions, he or she should separate the questions into two. Some questions can be used for practice before the exam, while the rest can be used as exam questions. - -If we illustrate this using our HDB resale price dataset, this means that we have to split the table into two. Out of 95868 entries, we will take some entries as out training dataset and leave the rest for out test dataset. The common proportion is either 70% or 80% for the training dataset and leave the other 30% or 20% for the test dataset. - -One important note is that the split must be done **randomly**. This is to avoid systematic bias in the split of the dataset. For example, one may enter the data according to the flat type and so flat with smaller rooms and smaller floor area will be on the top rows and those with the larger flats will be somewhere at the end of the rows. If we do not split the data randomly, we may not have any larger flats in our training set and only use the smaller flats. Similarly it can happen with any other column such as the block or the town area. - -There are times in machine learning, we need to experiment with different parameters and find the optimum parameters. In these cases, the dataset is usually split into three: -- **training** dataset, which is used to build the model -- **validation** dataset, which is used to evaluate the model for various parameters and to choose the optimum parameter -- **test** dataset, which is used to evaluate the model built with the optimum parameter found previously - -You will see some of these application in the subsequent weeks. diff --git a/_Notes/overview.md b/_Notes/overview.md index 9f227cc..bea7dc0 100644 --- a/_Notes/overview.md +++ b/_Notes/overview.md @@ -1,5 +1,5 @@ --- -title: Data Driven World Notes +title: Introduction to Computational Thinking and Problem Solving Using Python permalink: /notes/overview key: notes-overview layout: article @@ -13,74 +13,72 @@ show_edit_on_github: false show_date: false --- -## Week 01: Python Revision and Sorting Algorithm +## Lesson 0: Computational Thinking and Problem Solving Notes: -- [Bubble Sort and Insertion Sort]({% link _Notes/BubbleSort_InsertionSort.md %}) - -## Week 02: Analysing Programs +- [What is Computational Thinking]({{ "/notes/intro-ct" | relative_url }}) +- [Computational Thinking and Programming]({{ "/notes/intro-programming" | relative_url }}) +- [Problem Solving Framework]({{ "/notes/intro-pcdit" | relative_url }}) +## Lesson 1: Code Execution and Basic Data Types Notes: -- [Binary Heap and Heapsort]({% link _Notes/BinaryHeap_Heapsort.md %}) -- [Analysing Computation Time]({% link _Notes/ComputationTime.md %}) - -## Week 03: Divide and Conquer - -Notes: +- [Hello World]({{ "/notes/hello-world" | relative_url }}) +- [Basic Data Types]({{ "/notes/basic-data-types" | relative_url }}) -- [Divide and Conquer]({% link _Notes/Divide_Conquer.md %}) -- [Merge Sort]({% link _Notes/Merge_Sort.md %}) -## Week 04: Object-Oriented Paradigm +## Lesson 2: Function, the First Abstraction Notes: -- [Object Oriented Programming]({% link _Notes/Object_Oriented_Programming.md %}) -- [Linear Data Structures]({% link _Notes/Linear_Data_Structures.md %}) +- [Calling a Function]({{ "/notes/calling-function" | relative_url }}) +- [Defining a Function]({{ "/notes/defining-function" | relative_url }}) + -## Week 05: Searching Data +## Lesson 3: Basic Operators and Basic Control Structures Notes: -- [Introduction to Graph]({% link _Notes/Intro_to_Graph.md %}) -- [Graph Search]({% link _Notes/Graph_Search.md %}) +- [Basic Operators]({{ "/notes/basic-operators" | relative_url }}) +- [Basic Control Structures]({{ "/notes/basic-structures" | relative_url }}) -## Week 06: Inheritance and Object-Oriented Design +## Lesson 4: Boolean Data Type and Branch Structure Notes: -- [Inheritance and Abstract Base Class]({% link _Notes/Inheritance_ABC.md %}) -- [Fixed-Size Array and Linked List]({%link _Notes/Array_LinkedList.md %}) +- [Boolean Data]({{ "/notes/boolean-data" | relative_url }}) +- [Branch Structures]({{ "/notes/branch" | relative_url }}) -## Week 08: Visualizing and Processing Data +## Lesson 5: String Notes: -- [Working With Data]({% link _Notes/Working_With_Data.md %}) -- [Creating Plots using Matplotlib and Seaborn]({% link _Notes/Visualization.md %}) +- [Working with String]({{ "/notes/string" | relative_url }}) -## Week 09: Modeling Continuous Data +## Lesson 6: Iteration Using While Loop and For Loop Notes: -- [Linear Regression]({% link _Notes/LinearRegression.md %}) -- [Multiple Linear Regression]({% link _Notes/Multiple_Linear_Regression.md %}) +- [Implementing For Loop]({{ "/notes/for-loop" | relative_url }}) +- [Implementing While Loop]({{ "/notes/while-loop" | relative_url }}) -## Week 10: Classifying Categorical Data +## Lesson 7: Tuple and List Notes: -- [Logistic Regression]({% link _Notes/Logistic_Regression.md %}) -- [Confusion Matrix and Metrics]({% link _Notes/Confusion_Matrix_Metrics.md %}) +- [Working with Tuple]({{ "/notes/tuple" | relative_url }}) +- [Working with List]({{ "/notes/list" | relative_url }}) + +## Lesson 8: Nested List and Nested For Loop -## Week 12: Design of State Machines +- [Nested List]({{ "/notes/nested-list" | relative_url }}) +- [Nested For Loop]({{ "/notes/nested-for" | relative_url }}) -- [State Machine]({% link _Notes/State_Machine.md %}) -- [SM Abstract Class]({% link _Notes/StateMachine_ABC.md %}) -- [State Space Search]({% link _Notes/State_Space_Search.md %}) +## Lesson 9: Dictionaries and Sets +- [Working with Dictionary]({{ "/notes/dictionary" | relative_url }}) +- [Working with Set]({{ "/notes/set" | relative_url }}) ## Contributors -These notes and problem sets were prepared by Oka Kurniawan, Zachary Teo Wei Jie, and Amanda Kosim. +These notes and problem sets were prepared by Oka Kurniawan. \ No newline at end of file diff --git a/_config.yml b/_config.yml index 9d894e2..7bfc654 100644 --- a/_config.yml +++ b/_config.yml @@ -17,10 +17,11 @@ text_skin_alt: "dark" # "default" (default), "dark", "forest", "ocean", "chocola highlight_theme_alt: "tomorrow-night" # "default" (default), "tomorrow", "tomorrow-night", "tomorrow-night-eighties", "tomorrow-night-blue", "tomorrow-night-bright" text_skin: "default" # "default" (default), "dark", "forest", "ocean", "chocolate", "orange" highlight_theme: "tomorrow" # url: "https://username.github.io" -# baseurl: /baseurl # does not include hostname -title: 10.020 DDW +baseurl: "/ictpsp" # does not include hostname +url: "https://kurniawano.github.io" +title: ICTPSP description: > # this means to ignore newlines until "Language & timezone" - Welcome to 10.020 Data Driven World Course Site! + Welcome to Introduction to Computational Thinking and Problem Solving Using Python Book Site! ## => Language and Timezone ############################## @@ -49,14 +50,14 @@ author: ## => GitHub Repository (if the site is hosted by GitHub), won't be useful if you don't enable _show_edit_on_github ############################## -repository: Data-Driven-World/Data-Driven-World.github.io +repository: Data-Driven-World/ictpsp.github.io repository_tree: main ## => Paths ############################## paths: - root: # title link url, "/" (default) - home: # home layout url, "/" (default) + root: # title link url, "/" (default) + home: # home layout url, "/" (default) archive: # "/archive.html" (default) rss: # "/feed.xml" (default) @@ -149,7 +150,7 @@ collections: Notes: output: true Mini_Projects: - output: true + output: false Learning_Objectives: output: true diff --git a/_data/navigation.yml b/_data/navigation.yml index 8d60a0b..780e5cf 100644 --- a/_data/navigation.yml +++ b/_data/navigation.yml @@ -1,101 +1,71 @@ header: - title: Home url: /index.html - - title: Roadmap - url: /roadmap.html - title: Notes url: /notes/overview key: notes - - title: Mini Projects - url: /mini_projects/overview - key: miniprojects - title: Learning Objectives url: /lo/weekly key: lo-weekly -MiniProjects: - - title: Background - children: - - title: Getting Started with CLI - url: /mini_projects/background-cli - - title: Web Basics - url: /mini_projects/background-web - - title: Debug Notes - url: /mini_projects/debug-notes - - title: Mini Project 1 - children: - - title: MP Sort - url: https://github.com/Data-Driven-World/d2w_mini_projects/blob/master/mp_sort/Readme.md - - title: Mini Project 2 - children: - - title: MP Calc - url: https://github.com/Data-Driven-World/d2w_mini_projects/blob/master/mp_calc/Readme.md - - title: Mini Project 3 - children: - - title: MP TicTacToe - url: https://github.com/Data-Driven-World/d2w_mini_projects/blob/master/mp_tictactoe/Readme.md - - title: Grading - children: - - title: Checkoff Protocol - url: /mini_projects/checkoff Notes: - - title: Week 1 + - title: "Lesson 0: Computational Thinking and Problem Solving" children: - - title: Sorting Algorithms - url: /notes/bubble_insertion_sort - - title: Week 2 + - title: What is Computational Thinking + url: /notes/intro-ct + - title: Computational Thinking and Programming + url: /notes/intro-programming + - title: Problem Solving Framework + url: /notes/intro-pcdit + - title: "Lesson 1: Code Execution and Basic Data Types" children: - - title: Binary Heap - url: /notes/binary_heap_sort - - title: Computation Time - url: /notes/computation_time - - title: Week 3 + - title: Hello World + url: /notes/hello-world + - title: Basic Data Types + url: /notes/basic-data-types + - title: "Lesson 2: Function, the First Abstraction" children: - - title: Divide and Conquer - url: /notes/divide_conquer - - title: Merge Sort - url: /notes/merge_sort - - title: Week 4 + - title: Calling a Function + url: /notes/calling-function + - title: Defining a Function + url: /notes/defining-function + - title: "Lesson 3: Operators and Operands" children: - - title: OOP - url: /notes/oo_programming - - title: Linear Data Structures - url: /notes/linear_data_structure - - title: Week 5 + - title: Basic Operators + url: /notes/basic-operators + - title: Basic Control Structures + url: /notes/basic-structures + - title: "Lesson 4: Boolean Data Type and Branch Structure" children: - - title: Basics of Graph - url: /notes/intro_graph - - title: Graph Search - url: /notes/graph_search - - title: Week 6 + - title: Boolean Data + url: /notes/boolean-data + - title: Branch Structures + url: /notes/branch + - title: "Lesson 5: String" children: - - title: Inheritance - url: /notes/inheritance_abc - - title: Array and Linked List - url: /notes/array_linkedlist - - title: Week 8 + - title: Working with String + url: /notes/string + - title: "Lesson 6: Iteration Using While Loop and For Loop" children: - - title: Working with Data - url: /notes/working_with_data - - title: Matplotlib and Seaborn - url: /notes/visualization - - title: Week 9 + - title: Implementing For Loop + url: /notes/for-loop + - title: Implementing While Loop + url: /notes/while-loop + - title: "Lesson 7: List and Tuple" children: - - title: Linear Regression - url: /notes/linear_regression - - title: Multiple Linear Regression - url: /notes/multiple_linear_regression - - title: Week 10 + - title: Working with Tuple + url: /notes/tuple + - title: Working with List + url: /notes/list + - title: "Lesson 8: Nested List and Nested For Loop" children: - - title: Logistic Regression - url: /notes/logistic_regression - - title: Confusion Matrix and Metrics - url: /notes/confusion_matrix_metrics - - title: Week 12 + - title: Nested List + url: /notes/nested-list + - title: Nested For Loop + url: /notes/nested-for + - title: "Lesson 9: Dictionary and Set" children: - - title: State Machine - url: /notes/state_machine - - title: SM Abstract Class - url: /notes/abstract_sm_class - - title: State Space Search - url: /notes/state_space_search + - title: Working with Dictionary + url: /notes/dictionary + - title: Working with Set + url: /notes/set \ No newline at end of file diff --git a/_includes/scripts/utils/utils.js b/_includes/scripts/utils/utils.js index 4b7d46a..0c74320 100644 --- a/_includes/scripts/utils/utils.js +++ b/_includes/scripts/utils/utils.js @@ -3,8 +3,8 @@ function showContent() { document.body.style.opacity = 1; } -var lightStyle = '@import url("/assets/css/main.css");'; -var darkStyle = '@import url("/assets/css/main_alt.css");'; +var lightStyle = '@import url("/ictpsp/assets/css/main.css");'; +var darkStyle = '@import url("/ictpsp/assets/css/main_alt.css");'; function setTheme(theme) { if (theme == "dark") { diff --git a/assets/images/lesson0/compiler.png b/assets/images/lesson0/compiler.png new file mode 100644 index 0000000..109263e Binary files /dev/null and b/assets/images/lesson0/compiler.png differ diff --git a/assets/images/lesson0/interpreter.png b/assets/images/lesson0/interpreter.png new file mode 100644 index 0000000..4f042d8 Binary files /dev/null and b/assets/images/lesson0/interpreter.png differ diff --git a/assets/images/lesson0/pcdit_figure_new.png b/assets/images/lesson0/pcdit_figure_new.png new file mode 100644 index 0000000..008dc9f Binary files /dev/null and b/assets/images/lesson0/pcdit_figure_new.png differ diff --git a/assets/images/lesson0/programming_skills.png b/assets/images/lesson0/programming_skills.png new file mode 100644 index 0000000..aadd7c8 Binary files /dev/null and b/assets/images/lesson0/programming_skills.png differ diff --git a/assets/images/lesson2/assignment.png b/assets/images/lesson2/assignment.png new file mode 100644 index 0000000..fd6e8a0 Binary files /dev/null and b/assets/images/lesson2/assignment.png differ diff --git a/assets/images/lesson2/function_to_var.png b/assets/images/lesson2/function_to_var.png new file mode 100644 index 0000000..fb47727 Binary files /dev/null and b/assets/images/lesson2/function_to_var.png differ diff --git a/assets/images/lesson2/functions.png b/assets/images/lesson2/functions.png new file mode 100644 index 0000000..1afb9f8 Binary files /dev/null and b/assets/images/lesson2/functions.png differ diff --git a/assets/images/lesson2/functions_i_o.png b/assets/images/lesson2/functions_i_o.png new file mode 100644 index 0000000..30d394c Binary files /dev/null and b/assets/images/lesson2/functions_i_o.png differ diff --git a/assets/images/lesson2/functions_no_i_no_o.png b/assets/images/lesson2/functions_no_i_no_o.png new file mode 100644 index 0000000..14c77d3 Binary files /dev/null and b/assets/images/lesson2/functions_no_i_no_o.png differ diff --git a/assets/images/lesson3/average.png b/assets/images/lesson3/average.png new file mode 100644 index 0000000..722171a Binary files /dev/null and b/assets/images/lesson3/average.png differ diff --git a/assets/images/lesson3/average_flowchart.png b/assets/images/lesson3/average_flowchart.png new file mode 100644 index 0000000..7dfb198 Binary files /dev/null and b/assets/images/lesson3/average_flowchart.png differ diff --git a/assets/images/lesson3/branch.png b/assets/images/lesson3/branch.png new file mode 100644 index 0000000..6246181 Binary files /dev/null and b/assets/images/lesson3/branch.png differ diff --git a/assets/images/lesson3/branch1.png b/assets/images/lesson3/branch1.png new file mode 100644 index 0000000..fe5cd10 Binary files /dev/null and b/assets/images/lesson3/branch1.png differ diff --git a/assets/images/lesson3/branch2.png b/assets/images/lesson3/branch2.png new file mode 100644 index 0000000..054d458 Binary files /dev/null and b/assets/images/lesson3/branch2.png differ diff --git a/assets/images/lesson3/control_structures.png b/assets/images/lesson3/control_structures.png new file mode 100644 index 0000000..17efd39 Binary files /dev/null and b/assets/images/lesson3/control_structures.png differ diff --git a/assets/images/lesson3/decision.png b/assets/images/lesson3/decision.png new file mode 100644 index 0000000..5a66f8a Binary files /dev/null and b/assets/images/lesson3/decision.png differ diff --git a/assets/images/lesson3/io.png b/assets/images/lesson3/io.png new file mode 100644 index 0000000..ab3e67e Binary files /dev/null and b/assets/images/lesson3/io.png differ diff --git a/assets/images/lesson3/iterative1.png b/assets/images/lesson3/iterative1.png new file mode 100644 index 0000000..ae64a19 Binary files /dev/null and b/assets/images/lesson3/iterative1.png differ diff --git a/assets/images/lesson3/loop_flowchart.png b/assets/images/lesson3/loop_flowchart.png new file mode 100644 index 0000000..d089582 Binary files /dev/null and b/assets/images/lesson3/loop_flowchart.png differ diff --git a/assets/images/lesson3/multiply.png b/assets/images/lesson3/multiply.png new file mode 100644 index 0000000..c126568 Binary files /dev/null and b/assets/images/lesson3/multiply.png differ diff --git a/assets/images/lesson3/overall_flowchart.png b/assets/images/lesson3/overall_flowchart.png new file mode 100644 index 0000000..5980e7e Binary files /dev/null and b/assets/images/lesson3/overall_flowchart.png differ diff --git a/assets/images/lesson3/process.png b/assets/images/lesson3/process.png new file mode 100644 index 0000000..32b2a12 Binary files /dev/null and b/assets/images/lesson3/process.png differ diff --git a/assets/images/lesson3/sequential1.png b/assets/images/lesson3/sequential1.png new file mode 100644 index 0000000..fc65b0f Binary files /dev/null and b/assets/images/lesson3/sequential1.png differ diff --git a/assets/images/lesson3/sequential2.png b/assets/images/lesson3/sequential2.png new file mode 100644 index 0000000..9c99b30 Binary files /dev/null and b/assets/images/lesson3/sequential2.png differ diff --git a/assets/images/lesson3/sequential_average_status.png b/assets/images/lesson3/sequential_average_status.png new file mode 100644 index 0000000..ac56894 Binary files /dev/null and b/assets/images/lesson3/sequential_average_status.png differ diff --git a/assets/images/lesson3/state_decision.png b/assets/images/lesson3/state_decision.png new file mode 100644 index 0000000..ac3dfd8 Binary files /dev/null and b/assets/images/lesson3/state_decision.png differ diff --git a/assets/images/lesson3/terminal.png b/assets/images/lesson3/terminal.png new file mode 100644 index 0000000..466766c Binary files /dev/null and b/assets/images/lesson3/terminal.png differ diff --git a/assets/images/lesson4/if_elif_else.png b/assets/images/lesson4/if_elif_else.png new file mode 100644 index 0000000..5e28fe0 Binary files /dev/null and b/assets/images/lesson4/if_elif_else.png differ diff --git a/assets/images/lesson4/if_else_flowchart.png b/assets/images/lesson4/if_else_flowchart.png new file mode 100644 index 0000000..9806af6 Binary files /dev/null and b/assets/images/lesson4/if_else_flowchart.png differ diff --git a/assets/images/lesson4/if_else_next.png b/assets/images/lesson4/if_else_next.png new file mode 100644 index 0000000..89fff46 Binary files /dev/null and b/assets/images/lesson4/if_else_next.png differ diff --git a/assets/images/lesson4/if_if_else.png b/assets/images/lesson4/if_if_else.png new file mode 100644 index 0000000..0f47071 Binary files /dev/null and b/assets/images/lesson4/if_if_else.png differ diff --git a/assets/images/lesson4/if_true_flowchart.png b/assets/images/lesson4/if_true_flowchart.png new file mode 100644 index 0000000..e64bb01 Binary files /dev/null and b/assets/images/lesson4/if_true_flowchart.png differ diff --git a/assets/images/lesson4/if_true_outside.png b/assets/images/lesson4/if_true_outside.png new file mode 100644 index 0000000..ec521ba Binary files /dev/null and b/assets/images/lesson4/if_true_outside.png differ diff --git a/assets/images/lesson4/nested_if.png b/assets/images/lesson4/nested_if.png new file mode 100644 index 0000000..e39dda3 Binary files /dev/null and b/assets/images/lesson4/nested_if.png differ diff --git a/assets/images/lesson6/for_in_flowchart.png b/assets/images/lesson6/for_in_flowchart.png new file mode 100644 index 0000000..dc5d4b1 Binary files /dev/null and b/assets/images/lesson6/for_in_flowchart.png differ diff --git a/assets/images/lesson6/while_flowchart.png b/assets/images/lesson6/while_flowchart.png new file mode 100644 index 0000000..658233b Binary files /dev/null and b/assets/images/lesson6/while_flowchart.png differ diff --git a/assets/images/lesson7/list_slicing_indices.png b/assets/images/lesson7/list_slicing_indices.png new file mode 100644 index 0000000..f2e9098 Binary files /dev/null and b/assets/images/lesson7/list_slicing_indices.png differ diff --git a/assets/images/lesson9/a_minus_b.png b/assets/images/lesson9/a_minus_b.png new file mode 100644 index 0000000..efc7299 Binary files /dev/null and b/assets/images/lesson9/a_minus_b.png differ diff --git a/assets/images/lesson9/intersection.png b/assets/images/lesson9/intersection.png new file mode 100644 index 0000000..4819086 Binary files /dev/null and b/assets/images/lesson9/intersection.png differ diff --git a/assets/images/lesson9/mrt_map.png b/assets/images/lesson9/mrt_map.png new file mode 100644 index 0000000..1f3edd0 Binary files /dev/null and b/assets/images/lesson9/mrt_map.png differ diff --git a/assets/images/lesson9/park_map.png b/assets/images/lesson9/park_map.png new file mode 100644 index 0000000..0b0f2a8 Binary files /dev/null and b/assets/images/lesson9/park_map.png differ diff --git a/assets/images/lesson9/park_map_abc.png b/assets/images/lesson9/park_map_abc.png new file mode 100644 index 0000000..5e44c1d Binary files /dev/null and b/assets/images/lesson9/park_map_abc.png differ diff --git a/assets/images/lesson9/subset.png b/assets/images/lesson9/subset.png new file mode 100644 index 0000000..3a28693 Binary files /dev/null and b/assets/images/lesson9/subset.png differ diff --git a/assets/images/lesson9/symmetric_difference.png b/assets/images/lesson9/symmetric_difference.png new file mode 100644 index 0000000..631d328 Binary files /dev/null and b/assets/images/lesson9/symmetric_difference.png differ diff --git a/assets/images/lesson9/union.png b/assets/images/lesson9/union.png new file mode 100644 index 0000000..ebb8ccd Binary files /dev/null and b/assets/images/lesson9/union.png differ diff --git a/assets/images/week1/1.png b/assets/images/week1/1.png deleted file mode 100644 index 2e9e64a..0000000 Binary files a/assets/images/week1/1.png and /dev/null differ diff --git a/assets/images/week1/10.png b/assets/images/week1/10.png deleted file mode 100644 index b54e196..0000000 Binary files a/assets/images/week1/10.png and /dev/null differ diff --git a/assets/images/week1/11.png b/assets/images/week1/11.png deleted file mode 100644 index b979216..0000000 Binary files a/assets/images/week1/11.png and /dev/null differ diff --git a/assets/images/week1/12.png b/assets/images/week1/12.png deleted file mode 100644 index 9851bb5..0000000 Binary files a/assets/images/week1/12.png and /dev/null differ diff --git a/assets/images/week1/13.png b/assets/images/week1/13.png deleted file mode 100644 index 7750952..0000000 Binary files a/assets/images/week1/13.png and /dev/null differ diff --git a/assets/images/week1/14.png b/assets/images/week1/14.png deleted file mode 100644 index a26a696..0000000 Binary files a/assets/images/week1/14.png and /dev/null differ diff --git a/assets/images/week1/15.png b/assets/images/week1/15.png deleted file mode 100644 index 0587c6d..0000000 Binary files a/assets/images/week1/15.png and /dev/null differ diff --git a/assets/images/week1/16.png b/assets/images/week1/16.png deleted file mode 100644 index 1b598f0..0000000 Binary files a/assets/images/week1/16.png and /dev/null differ diff --git a/assets/images/week1/17.png b/assets/images/week1/17.png deleted file mode 100644 index 8cf0b80..0000000 Binary files a/assets/images/week1/17.png and /dev/null differ diff --git a/assets/images/week1/18.png b/assets/images/week1/18.png deleted file mode 100644 index 98309dc..0000000 Binary files a/assets/images/week1/18.png and /dev/null differ diff --git a/assets/images/week1/2.png b/assets/images/week1/2.png deleted file mode 100644 index c8cf255..0000000 Binary files a/assets/images/week1/2.png and /dev/null differ diff --git a/assets/images/week1/3.png b/assets/images/week1/3.png deleted file mode 100644 index 46a29a1..0000000 Binary files a/assets/images/week1/3.png and /dev/null differ diff --git a/assets/images/week1/4.png b/assets/images/week1/4.png deleted file mode 100644 index 53ec5c9..0000000 Binary files a/assets/images/week1/4.png and /dev/null differ diff --git a/assets/images/week1/5.png b/assets/images/week1/5.png deleted file mode 100644 index 31841f5..0000000 Binary files a/assets/images/week1/5.png and /dev/null differ diff --git a/assets/images/week1/6.png b/assets/images/week1/6.png deleted file mode 100644 index 303ddd8..0000000 Binary files a/assets/images/week1/6.png and /dev/null differ diff --git a/assets/images/week1/7.png b/assets/images/week1/7.png deleted file mode 100644 index 579c274..0000000 Binary files a/assets/images/week1/7.png and /dev/null differ diff --git a/assets/images/week1/9.png b/assets/images/week1/9.png deleted file mode 100644 index 09d28b5..0000000 Binary files a/assets/images/week1/9.png and /dev/null differ diff --git a/assets/images/week10/Logistic_Regression_4_1.jpeg b/assets/images/week10/Logistic_Regression_4_1.jpeg deleted file mode 100644 index a62a322..0000000 Binary files a/assets/images/week10/Logistic_Regression_4_1.jpeg and /dev/null differ diff --git a/assets/images/week10/Logistic_Regression_4_1.png b/assets/images/week10/Logistic_Regression_4_1.png deleted file mode 100644 index 771b42a..0000000 Binary files a/assets/images/week10/Logistic_Regression_4_1.png and /dev/null differ diff --git a/assets/images/week10/cancer_cell_plot.png b/assets/images/week10/cancer_cell_plot.png deleted file mode 100644 index f37c6da..0000000 Binary files a/assets/images/week10/cancer_cell_plot.png and /dev/null differ diff --git a/assets/images/week10/confusion_matrix_accuracy.png b/assets/images/week10/confusion_matrix_accuracy.png deleted file mode 100644 index edb201f..0000000 Binary files a/assets/images/week10/confusion_matrix_accuracy.png and /dev/null differ diff --git a/assets/images/week10/confusion_matrix_precision.png b/assets/images/week10/confusion_matrix_precision.png deleted file mode 100644 index 035894d..0000000 Binary files a/assets/images/week10/confusion_matrix_precision.png and /dev/null differ diff --git a/assets/images/week10/confusion_matrix_sensitivity.png b/assets/images/week10/confusion_matrix_sensitivity.png deleted file mode 100644 index 0d482b6..0000000 Binary files a/assets/images/week10/confusion_matrix_sensitivity.png and /dev/null differ diff --git a/assets/images/week10/confusion_matrix_specificity.png b/assets/images/week10/confusion_matrix_specificity.png deleted file mode 100644 index 919cb41..0000000 Binary files a/assets/images/week10/confusion_matrix_specificity.png and /dev/null differ diff --git a/assets/images/week10/decision_boundary.png b/assets/images/week10/decision_boundary.png deleted file mode 100644 index 15aef6f..0000000 Binary files a/assets/images/week10/decision_boundary.png and /dev/null differ diff --git a/assets/images/week12/SM_class.png b/assets/images/week12/SM_class.png deleted file mode 100644 index 5cb38ca..0000000 Binary files a/assets/images/week12/SM_class.png and /dev/null differ diff --git a/assets/images/week12/SearchNode_class_UML.png b/assets/images/week12/SearchNode_class_UML.png deleted file mode 100644 index 41ec469..0000000 Binary files a/assets/images/week12/SearchNode_class_UML.png and /dev/null differ diff --git a/assets/images/week12/coke_sm.jpeg b/assets/images/week12/coke_sm.jpeg deleted file mode 100644 index 1d91f71..0000000 Binary files a/assets/images/week12/coke_sm.jpeg and /dev/null differ diff --git a/assets/images/week12/coke_sm.png b/assets/images/week12/coke_sm.png deleted file mode 100755 index 879c4e7..0000000 Binary files a/assets/images/week12/coke_sm.png and /dev/null differ diff --git a/assets/images/week12/lightbox_state_transition_diagram.jpeg b/assets/images/week12/lightbox_state_transition_diagram.jpeg deleted file mode 100644 index aa2bc97..0000000 Binary files a/assets/images/week12/lightbox_state_transition_diagram.jpeg and /dev/null differ diff --git a/assets/images/week12/lightbox_state_transition_diagram.png b/assets/images/week12/lightbox_state_transition_diagram.png deleted file mode 100644 index cfb3ca4..0000000 Binary files a/assets/images/week12/lightbox_state_transition_diagram.png and /dev/null differ diff --git a/assets/images/week12/state_search_trees.jpeg b/assets/images/week12/state_search_trees.jpeg deleted file mode 100644 index 738f106..0000000 Binary files a/assets/images/week12/state_search_trees.jpeg and /dev/null differ diff --git a/assets/images/week12/state_search_trees.png b/assets/images/week12/state_search_trees.png deleted file mode 100644 index 196891e..0000000 Binary files a/assets/images/week12/state_search_trees.png and /dev/null differ diff --git a/assets/images/week12/state_space_map.jpeg b/assets/images/week12/state_space_map.jpeg deleted file mode 100644 index bb84e56..0000000 Binary files a/assets/images/week12/state_space_map.jpeg and /dev/null differ diff --git a/assets/images/week12/state_space_map.png b/assets/images/week12/state_space_map.png deleted file mode 100644 index 3876870..0000000 Binary files a/assets/images/week12/state_space_map.png and /dev/null differ diff --git a/assets/images/week2/1.png b/assets/images/week2/1.png deleted file mode 100644 index b7057de..0000000 Binary files a/assets/images/week2/1.png and /dev/null differ diff --git a/assets/images/week2/Binary_Heap.png b/assets/images/week2/Binary_Heap.png deleted file mode 100644 index 9c433c6..0000000 Binary files a/assets/images/week2/Binary_Heap.png and /dev/null differ diff --git a/assets/images/week2/Binary_Heap_Array.png b/assets/images/week2/Binary_Heap_Array.png deleted file mode 100644 index d9d4976..0000000 Binary files a/assets/images/week2/Binary_Heap_Array.png and /dev/null differ diff --git a/assets/images/week2/Build_Heap_1.png b/assets/images/week2/Build_Heap_1.png deleted file mode 100644 index 7b63de6..0000000 Binary files a/assets/images/week2/Build_Heap_1.png and /dev/null differ diff --git a/assets/images/week2/Build_Heap_2.png b/assets/images/week2/Build_Heap_2.png deleted file mode 100644 index 7d0db8e..0000000 Binary files a/assets/images/week2/Build_Heap_2.png and /dev/null differ diff --git a/assets/images/week2/Build_Heap_3.png b/assets/images/week2/Build_Heap_3.png deleted file mode 100644 index 2e49e8a..0000000 Binary files a/assets/images/week2/Build_Heap_3.png and /dev/null differ diff --git a/assets/images/week2/Build_Heap_4.png b/assets/images/week2/Build_Heap_4.png deleted file mode 100644 index 168b3a1..0000000 Binary files a/assets/images/week2/Build_Heap_4.png and /dev/null differ diff --git a/assets/images/week2/Build_Heap_5.png b/assets/images/week2/Build_Heap_5.png deleted file mode 100644 index 3613e00..0000000 Binary files a/assets/images/week2/Build_Heap_5.png and /dev/null differ diff --git a/assets/images/week2/Build_Heap_6.png b/assets/images/week2/Build_Heap_6.png deleted file mode 100644 index c9b79c4..0000000 Binary files a/assets/images/week2/Build_Heap_6.png and /dev/null differ diff --git a/assets/images/week2/Build_Heap_7.png b/assets/images/week2/Build_Heap_7.png deleted file mode 100644 index 6953c58..0000000 Binary files a/assets/images/week2/Build_Heap_7.png and /dev/null differ diff --git a/assets/images/week2/Build_Heap_8.png b/assets/images/week2/Build_Heap_8.png deleted file mode 100644 index d37bd1f..0000000 Binary files a/assets/images/week2/Build_Heap_8.png and /dev/null differ diff --git a/assets/images/week2/Build_Heap_9.png b/assets/images/week2/Build_Heap_9.png deleted file mode 100644 index 56ae84e..0000000 Binary files a/assets/images/week2/Build_Heap_9.png and /dev/null differ diff --git a/assets/images/week2/Heapify_1.png b/assets/images/week2/Heapify_1.png deleted file mode 100644 index 2133e26..0000000 Binary files a/assets/images/week2/Heapify_1.png and /dev/null differ diff --git a/assets/images/week2/Heapify_2.png b/assets/images/week2/Heapify_2.png deleted file mode 100644 index dffb574..0000000 Binary files a/assets/images/week2/Heapify_2.png and /dev/null differ diff --git a/assets/images/week2/Heapify_3.png b/assets/images/week2/Heapify_3.png deleted file mode 100644 index 9062981..0000000 Binary files a/assets/images/week2/Heapify_3.png and /dev/null differ diff --git a/assets/images/week2/plot_time_bubblesort.jpeg b/assets/images/week2/plot_time_bubblesort.jpeg deleted file mode 100644 index 7b728bb..0000000 Binary files a/assets/images/week2/plot_time_bubblesort.jpeg and /dev/null differ diff --git a/assets/images/week2/plot_time_bubblesort.png b/assets/images/week2/plot_time_bubblesort.png deleted file mode 100644 index 6a0bfbe..0000000 Binary files a/assets/images/week2/plot_time_bubblesort.png and /dev/null differ diff --git a/assets/images/week2/plot_time_bubblesort_sorted.jpeg b/assets/images/week2/plot_time_bubblesort_sorted.jpeg deleted file mode 100644 index f91fb2f..0000000 Binary files a/assets/images/week2/plot_time_bubblesort_sorted.jpeg and /dev/null differ diff --git a/assets/images/week2/plot_time_bubblesort_sorted.png b/assets/images/week2/plot_time_bubblesort_sorted.png deleted file mode 100644 index da6e2eb..0000000 Binary files a/assets/images/week2/plot_time_bubblesort_sorted.png and /dev/null differ diff --git a/assets/images/week2/plot_time_heapsort_random.jpeg b/assets/images/week2/plot_time_heapsort_random.jpeg deleted file mode 100644 index e608134..0000000 Binary files a/assets/images/week2/plot_time_heapsort_random.jpeg and /dev/null differ diff --git a/assets/images/week2/plot_time_heapsort_random.png b/assets/images/week2/plot_time_heapsort_random.png deleted file mode 100644 index 35b7e16..0000000 Binary files a/assets/images/week2/plot_time_heapsort_random.png and /dev/null differ diff --git a/assets/images/week2/plot_time_heapsort_random_xaxis.jpeg b/assets/images/week2/plot_time_heapsort_random_xaxis.jpeg deleted file mode 100644 index 2b88ca7..0000000 Binary files a/assets/images/week2/plot_time_heapsort_random_xaxis.jpeg and /dev/null differ diff --git a/assets/images/week2/plot_time_heapsort_random_xaxis.png b/assets/images/week2/plot_time_heapsort_random_xaxis.png deleted file mode 100644 index 79a1b25..0000000 Binary files a/assets/images/week2/plot_time_heapsort_random_xaxis.png and /dev/null differ diff --git a/assets/images/week2/plot_time_heapsort_sorted.jpeg b/assets/images/week2/plot_time_heapsort_sorted.jpeg deleted file mode 100644 index 134ca38..0000000 Binary files a/assets/images/week2/plot_time_heapsort_sorted.jpeg and /dev/null differ diff --git a/assets/images/week2/plot_time_heapsort_sorted.png b/assets/images/week2/plot_time_heapsort_sorted.png deleted file mode 100644 index ca01fff..0000000 Binary files a/assets/images/week2/plot_time_heapsort_sorted.png and /dev/null differ diff --git a/assets/images/week2/plot_time_insertionsort_random.jpeg b/assets/images/week2/plot_time_insertionsort_random.jpeg deleted file mode 100644 index 8cfb9d8..0000000 Binary files a/assets/images/week2/plot_time_insertionsort_random.jpeg and /dev/null differ diff --git a/assets/images/week2/plot_time_insertionsort_random.png b/assets/images/week2/plot_time_insertionsort_random.png deleted file mode 100644 index 52b8e95..0000000 Binary files a/assets/images/week2/plot_time_insertionsort_random.png and /dev/null differ diff --git a/assets/images/week2/plot_time_insertionsort_sorted.jpeg b/assets/images/week2/plot_time_insertionsort_sorted.jpeg deleted file mode 100644 index 29e87e3..0000000 Binary files a/assets/images/week2/plot_time_insertionsort_sorted.jpeg and /dev/null differ diff --git a/assets/images/week2/plot_time_insertionsort_sorted.png b/assets/images/week2/plot_time_insertionsort_sorted.png deleted file mode 100644 index 7a7b8fe..0000000 Binary files a/assets/images/week2/plot_time_insertionsort_sorted.png and /dev/null differ diff --git a/assets/images/week3/merge_steps01.png b/assets/images/week3/merge_steps01.png deleted file mode 100644 index e4a17d5..0000000 Binary files a/assets/images/week3/merge_steps01.png and /dev/null differ diff --git a/assets/images/week3/merge_steps02.png b/assets/images/week3/merge_steps02.png deleted file mode 100644 index c1dd385..0000000 Binary files a/assets/images/week3/merge_steps02.png and /dev/null differ diff --git a/assets/images/week3/merge_steps03.png b/assets/images/week3/merge_steps03.png deleted file mode 100644 index 757c562..0000000 Binary files a/assets/images/week3/merge_steps03.png and /dev/null differ diff --git a/assets/images/week3/merge_steps04.png b/assets/images/week3/merge_steps04.png deleted file mode 100644 index 7672a69..0000000 Binary files a/assets/images/week3/merge_steps04.png and /dev/null differ diff --git a/assets/images/week3/merge_steps05.png b/assets/images/week3/merge_steps05.png deleted file mode 100644 index 224fbf0..0000000 Binary files a/assets/images/week3/merge_steps05.png and /dev/null differ diff --git a/assets/images/week3/merge_steps06.png b/assets/images/week3/merge_steps06.png deleted file mode 100644 index 5942cc1..0000000 Binary files a/assets/images/week3/merge_steps06.png and /dev/null differ diff --git a/assets/images/week3/merge_steps07.png b/assets/images/week3/merge_steps07.png deleted file mode 100644 index fe1277d..0000000 Binary files a/assets/images/week3/merge_steps07.png and /dev/null differ diff --git a/assets/images/week3/merge_steps08.png b/assets/images/week3/merge_steps08.png deleted file mode 100644 index 279889b..0000000 Binary files a/assets/images/week3/merge_steps08.png and /dev/null differ diff --git a/assets/images/week3/merge_steps09.png b/assets/images/week3/merge_steps09.png deleted file mode 100644 index 7f32bfe..0000000 Binary files a/assets/images/week3/merge_steps09.png and /dev/null differ diff --git a/assets/images/week3/merge_steps10.png b/assets/images/week3/merge_steps10.png deleted file mode 100644 index 9a387aa..0000000 Binary files a/assets/images/week3/merge_steps10.png and /dev/null differ diff --git a/assets/images/week3/mergesort_merge.png b/assets/images/week3/mergesort_merge.png deleted file mode 100644 index 83eac22..0000000 Binary files a/assets/images/week3/mergesort_merge.png and /dev/null differ diff --git a/assets/images/week3/mergesort_split.png b/assets/images/week3/mergesort_split.png deleted file mode 100644 index bf87262..0000000 Binary files a/assets/images/week3/mergesort_split.png and /dev/null differ diff --git a/assets/images/week3/mergesort_tree.jpeg b/assets/images/week3/mergesort_tree.jpeg deleted file mode 100644 index db5981f..0000000 Binary files a/assets/images/week3/mergesort_tree.jpeg and /dev/null differ diff --git a/assets/images/week3/mergesort_tree.png b/assets/images/week3/mergesort_tree.png deleted file mode 100644 index bf21296..0000000 Binary files a/assets/images/week3/mergesort_tree.png and /dev/null differ diff --git a/assets/images/week3/sum_recursiontree.png b/assets/images/week3/sum_recursiontree.png deleted file mode 100644 index dee1479..0000000 Binary files a/assets/images/week3/sum_recursiontree.png and /dev/null differ diff --git a/assets/images/week3/tower_of_hanoi.jpeg b/assets/images/week3/tower_of_hanoi.jpeg deleted file mode 100644 index b64c7db..0000000 Binary files a/assets/images/week3/tower_of_hanoi.jpeg and /dev/null differ diff --git a/assets/images/week3/tower_of_hanoi.png b/assets/images/week3/tower_of_hanoi.png deleted file mode 100644 index 6c33989..0000000 Binary files a/assets/images/week3/tower_of_hanoi.png and /dev/null differ diff --git a/assets/images/week3/tower_of_hanoi_general.jpeg b/assets/images/week3/tower_of_hanoi_general.jpeg deleted file mode 100644 index d56d48e..0000000 Binary files a/assets/images/week3/tower_of_hanoi_general.jpeg and /dev/null differ diff --git a/assets/images/week3/tower_of_hanoi_general.png b/assets/images/week3/tower_of_hanoi_general.png deleted file mode 100644 index 291fd5e..0000000 Binary files a/assets/images/week3/tower_of_hanoi_general.png and /dev/null differ diff --git a/assets/images/week3/tower_of_hanoi_time_3.jpeg b/assets/images/week3/tower_of_hanoi_time_3.jpeg deleted file mode 100644 index f8ecae8..0000000 Binary files a/assets/images/week3/tower_of_hanoi_time_3.jpeg and /dev/null differ diff --git a/assets/images/week3/tower_of_hanoi_time_3.png b/assets/images/week3/tower_of_hanoi_time_3.png deleted file mode 100644 index 457c80f..0000000 Binary files a/assets/images/week3/tower_of_hanoi_time_3.png and /dev/null differ diff --git a/assets/images/week3/tower_of_hanoi_time_n.png b/assets/images/week3/tower_of_hanoi_time_n.png deleted file mode 100644 index cb3a62b..0000000 Binary files a/assets/images/week3/tower_of_hanoi_time_n.png and /dev/null differ diff --git a/assets/images/week4/UML_class_relationship.png b/assets/images/week4/UML_class_relationship.png deleted file mode 100644 index 2cbdd8a..0000000 Binary files a/assets/images/week4/UML_class_relationship.png and /dev/null differ diff --git a/assets/images/week4/basic_class_attr_method.png b/assets/images/week4/basic_class_attr_method.png deleted file mode 100644 index 7fa1d03..0000000 Binary files a/assets/images/week4/basic_class_attr_method.png and /dev/null differ diff --git a/assets/images/week4/basic_class_attr_method_type.png b/assets/images/week4/basic_class_attr_method_type.png deleted file mode 100644 index 94e760d..0000000 Binary files a/assets/images/week4/basic_class_attr_method_type.png and /dev/null differ diff --git a/assets/images/week4/queue_double_stack.png b/assets/images/week4/queue_double_stack.png deleted file mode 100644 index 41a2fb1..0000000 Binary files a/assets/images/week4/queue_double_stack.png and /dev/null differ diff --git a/assets/images/week4/queue_doublestack_enqueue.png b/assets/images/week4/queue_doublestack_enqueue.png deleted file mode 100644 index 9d597e9..0000000 Binary files a/assets/images/week4/queue_doublestack_enqueue.png and /dev/null differ diff --git a/assets/images/week4/stack_postfix.png b/assets/images/week4/stack_postfix.png deleted file mode 100644 index f40853a..0000000 Binary files a/assets/images/week4/stack_postfix.png and /dev/null differ diff --git a/assets/images/week5/Graph_Vertex_Relationship.jpg b/assets/images/week5/Graph_Vertex_Relationship.jpg deleted file mode 100644 index e710b9d..0000000 Binary files a/assets/images/week5/Graph_Vertex_Relationship.jpg and /dev/null differ diff --git a/assets/images/week5/Graph_Vertex_Relationship.png b/assets/images/week5/Graph_Vertex_Relationship.png deleted file mode 100644 index b9aecf8..0000000 Binary files a/assets/images/week5/Graph_Vertex_Relationship.png and /dev/null differ diff --git a/assets/images/week5/Graph_Vertex_Specification.jpg b/assets/images/week5/Graph_Vertex_Specification.jpg deleted file mode 100644 index 1ad9247..0000000 Binary files a/assets/images/week5/Graph_Vertex_Specification.jpg and /dev/null differ diff --git a/assets/images/week5/Graph_Vertex_Specification.png b/assets/images/week5/Graph_Vertex_Specification.png deleted file mode 100644 index 7f02bfe..0000000 Binary files a/assets/images/week5/Graph_Vertex_Specification.png and /dev/null differ diff --git a/assets/images/week5/Graphs.png b/assets/images/week5/Graphs.png deleted file mode 100644 index b2519a9..0000000 Binary files a/assets/images/week5/Graphs.png and /dev/null differ diff --git a/assets/images/week5/MRT_Train.png b/assets/images/week5/MRT_Train.png deleted file mode 100644 index 0984f52..0000000 Binary files a/assets/images/week5/MRT_Train.png and /dev/null differ diff --git a/assets/images/week5/bfs_colour.jpg b/assets/images/week5/bfs_colour.jpg deleted file mode 100644 index 8980691..0000000 Binary files a/assets/images/week5/bfs_colour.jpg and /dev/null differ diff --git a/assets/images/week5/bfs_colour.png b/assets/images/week5/bfs_colour.png deleted file mode 100644 index 9aa20a3..0000000 Binary files a/assets/images/week5/bfs_colour.png and /dev/null differ diff --git a/assets/images/week5/bfs_graph_example.jpg b/assets/images/week5/bfs_graph_example.jpg deleted file mode 100644 index 37f77ea..0000000 Binary files a/assets/images/week5/bfs_graph_example.jpg and /dev/null differ diff --git a/assets/images/week5/bfs_graph_example.png b/assets/images/week5/bfs_graph_example.png deleted file mode 100644 index 8cdb1a4..0000000 Binary files a/assets/images/week5/bfs_graph_example.png and /dev/null differ diff --git a/assets/images/week5/bfs_tree1.jpg b/assets/images/week5/bfs_tree1.jpg deleted file mode 100644 index 5b09a41..0000000 Binary files a/assets/images/week5/bfs_tree1.jpg and /dev/null differ diff --git a/assets/images/week5/bfs_tree1.png b/assets/images/week5/bfs_tree1.png deleted file mode 100644 index 13190a8..0000000 Binary files a/assets/images/week5/bfs_tree1.png and /dev/null differ diff --git a/assets/images/week5/bfs_tree2.jpg b/assets/images/week5/bfs_tree2.jpg deleted file mode 100644 index 3cfb265..0000000 Binary files a/assets/images/week5/bfs_tree2.jpg and /dev/null differ diff --git a/assets/images/week5/bfs_tree2.png b/assets/images/week5/bfs_tree2.png deleted file mode 100644 index b469903..0000000 Binary files a/assets/images/week5/bfs_tree2.png and /dev/null differ diff --git a/assets/images/week5/depth_first_forest.jpg b/assets/images/week5/depth_first_forest.jpg deleted file mode 100644 index c327b6a..0000000 Binary files a/assets/images/week5/depth_first_forest.jpg and /dev/null differ diff --git a/assets/images/week5/depth_first_forest.png b/assets/images/week5/depth_first_forest.png deleted file mode 100644 index eb1657e..0000000 Binary files a/assets/images/week5/depth_first_forest.png and /dev/null differ diff --git a/assets/images/week5/dfs_forest.jpg b/assets/images/week5/dfs_forest.jpg deleted file mode 100644 index d9530ca..0000000 Binary files a/assets/images/week5/dfs_forest.jpg and /dev/null differ diff --git a/assets/images/week5/dfs_forest.png b/assets/images/week5/dfs_forest.png deleted file mode 100644 index 1684fd4..0000000 Binary files a/assets/images/week5/dfs_forest.png and /dev/null differ diff --git a/assets/images/week5/dfs_graph.jpg b/assets/images/week5/dfs_graph.jpg deleted file mode 100644 index d3aa87f..0000000 Binary files a/assets/images/week5/dfs_graph.jpg and /dev/null differ diff --git a/assets/images/week5/dfs_graph.png b/assets/images/week5/dfs_graph.png deleted file mode 100644 index 41781f7..0000000 Binary files a/assets/images/week5/dfs_graph.png and /dev/null differ diff --git a/assets/images/week5/sorted_graph.jpg b/assets/images/week5/sorted_graph.jpg deleted file mode 100644 index 603ab66..0000000 Binary files a/assets/images/week5/sorted_graph.jpg and /dev/null differ diff --git a/assets/images/week5/sorted_graph.png b/assets/images/week5/sorted_graph.png deleted file mode 100644 index 09f29d5..0000000 Binary files a/assets/images/week5/sorted_graph.png and /dev/null differ diff --git a/assets/images/week5/topological_sort_finishingtime.jpg b/assets/images/week5/topological_sort_finishingtime.jpg deleted file mode 100644 index 87ac384..0000000 Binary files a/assets/images/week5/topological_sort_finishingtime.jpg and /dev/null differ diff --git a/assets/images/week5/topological_sort_finishingtime.png b/assets/images/week5/topological_sort_finishingtime.png deleted file mode 100644 index f1ed479..0000000 Binary files a/assets/images/week5/topological_sort_finishingtime.png and /dev/null differ diff --git a/assets/images/week5/topological_sort_graph.jpg b/assets/images/week5/topological_sort_graph.jpg deleted file mode 100644 index 5d6b951..0000000 Binary files a/assets/images/week5/topological_sort_graph.jpg and /dev/null differ diff --git a/assets/images/week5/topological_sort_graph.png b/assets/images/week5/topological_sort_graph.png deleted file mode 100644 index 31fdd8d..0000000 Binary files a/assets/images/week5/topological_sort_graph.png and /dev/null differ diff --git a/assets/images/week5/vertexsearch.jpg b/assets/images/week5/vertexsearch.jpg deleted file mode 100644 index a5c5e15..0000000 Binary files a/assets/images/week5/vertexsearch.jpg and /dev/null differ diff --git a/assets/images/week5/vertexsearch.png b/assets/images/week5/vertexsearch.png deleted file mode 100644 index baa8384..0000000 Binary files a/assets/images/week5/vertexsearch.png and /dev/null differ diff --git a/assets/images/week6/array_add_element.jpg b/assets/images/week6/array_add_element.jpg deleted file mode 100644 index 2ca595b..0000000 Binary files a/assets/images/week6/array_add_element.jpg and /dev/null differ diff --git a/assets/images/week6/array_add_element.png b/assets/images/week6/array_add_element.png deleted file mode 100644 index 579b088..0000000 Binary files a/assets/images/week6/array_add_element.png and /dev/null differ diff --git a/assets/images/week6/array_insert.jpg b/assets/images/week6/array_insert.jpg deleted file mode 100644 index 8e80fb1..0000000 Binary files a/assets/images/week6/array_insert.jpg and /dev/null differ diff --git a/assets/images/week6/array_insert.png b/assets/images/week6/array_insert.png deleted file mode 100644 index 657c0c3..0000000 Binary files a/assets/images/week6/array_insert.png and /dev/null differ diff --git a/assets/images/week6/array_memory.jpg b/assets/images/week6/array_memory.jpg deleted file mode 100644 index 2841f13..0000000 Binary files a/assets/images/week6/array_memory.jpg and /dev/null differ diff --git a/assets/images/week6/array_memory.png b/assets/images/week6/array_memory.png deleted file mode 100644 index 65793b2..0000000 Binary files a/assets/images/week6/array_memory.png and /dev/null differ diff --git a/assets/images/week6/fraction_mixedfraction.jpg b/assets/images/week6/fraction_mixedfraction.jpg deleted file mode 100644 index 1b698aa..0000000 Binary files a/assets/images/week6/fraction_mixedfraction.jpg and /dev/null differ diff --git a/assets/images/week6/fraction_mixedfraction.png b/assets/images/week6/fraction_mixedfraction.png deleted file mode 100644 index 2dc674a..0000000 Binary files a/assets/images/week6/fraction_mixedfraction.png and /dev/null differ diff --git a/assets/images/week6/linkedlist.jpg b/assets/images/week6/linkedlist.jpg deleted file mode 100644 index 29b0954..0000000 Binary files a/assets/images/week6/linkedlist.jpg and /dev/null differ diff --git a/assets/images/week6/linkedlist.png b/assets/images/week6/linkedlist.png deleted file mode 100644 index 2fbee49..0000000 Binary files a/assets/images/week6/linkedlist.png and /dev/null differ diff --git a/assets/images/week6/linkedlist_insert_first.jpg b/assets/images/week6/linkedlist_insert_first.jpg deleted file mode 100644 index f30c20f..0000000 Binary files a/assets/images/week6/linkedlist_insert_first.jpg and /dev/null differ diff --git a/assets/images/week6/linkedlist_insert_first.png b/assets/images/week6/linkedlist_insert_first.png deleted file mode 100644 index 7e2e4cd..0000000 Binary files a/assets/images/week6/linkedlist_insert_first.png and /dev/null differ diff --git a/assets/images/week6/linkedlist_insert_mid.jpg b/assets/images/week6/linkedlist_insert_mid.jpg deleted file mode 100644 index 927a5e4..0000000 Binary files a/assets/images/week6/linkedlist_insert_mid.jpg and /dev/null differ diff --git a/assets/images/week6/linkedlist_insert_mid.png b/assets/images/week6/linkedlist_insert_mid.png deleted file mode 100644 index acfc96e..0000000 Binary files a/assets/images/week6/linkedlist_insert_mid.png and /dev/null differ diff --git a/assets/images/week6/linkedlist_remove_first.jpg b/assets/images/week6/linkedlist_remove_first.jpg deleted file mode 100644 index 7f7c071..0000000 Binary files a/assets/images/week6/linkedlist_remove_first.jpg and /dev/null differ diff --git a/assets/images/week6/linkedlist_remove_first.png b/assets/images/week6/linkedlist_remove_first.png deleted file mode 100644 index f3eb954..0000000 Binary files a/assets/images/week6/linkedlist_remove_first.png and /dev/null differ diff --git a/assets/images/week6/linkedlist_remove_mid.jpg b/assets/images/week6/linkedlist_remove_mid.jpg deleted file mode 100644 index 7daa516..0000000 Binary files a/assets/images/week6/linkedlist_remove_mid.jpg and /dev/null differ diff --git a/assets/images/week6/linkedlist_remove_mid.png b/assets/images/week6/linkedlist_remove_mid.png deleted file mode 100644 index cc95334..0000000 Binary files a/assets/images/week6/linkedlist_remove_mid.png and /dev/null differ diff --git a/assets/images/week6/queue_deque.jpg b/assets/images/week6/queue_deque.jpg deleted file mode 100644 index a09ef36..0000000 Binary files a/assets/images/week6/queue_deque.jpg and /dev/null differ diff --git a/assets/images/week6/queue_deque.png b/assets/images/week6/queue_deque.png deleted file mode 100644 index ecb0d19..0000000 Binary files a/assets/images/week6/queue_deque.png and /dev/null differ diff --git a/assets/images/week6/uml_array_linkedlist.jpg b/assets/images/week6/uml_array_linkedlist.jpg deleted file mode 100644 index 4e299c8..0000000 Binary files a/assets/images/week6/uml_array_linkedlist.jpg and /dev/null differ diff --git a/assets/images/week6/uml_array_linkedlist.png b/assets/images/week6/uml_array_linkedlist.png deleted file mode 100644 index da95b86..0000000 Binary files a/assets/images/week6/uml_array_linkedlist.png and /dev/null differ diff --git a/assets/images/week8/Visualization_11_1.jpeg b/assets/images/week8/Visualization_11_1.jpeg deleted file mode 100644 index 143136f..0000000 Binary files a/assets/images/week8/Visualization_11_1.jpeg and /dev/null differ diff --git a/assets/images/week8/Visualization_11_1.png b/assets/images/week8/Visualization_11_1.png deleted file mode 100644 index 4cee604..0000000 Binary files a/assets/images/week8/Visualization_11_1.png and /dev/null differ diff --git a/assets/images/week8/Visualization_13_1.png b/assets/images/week8/Visualization_13_1.png deleted file mode 100644 index de8dc41..0000000 Binary files a/assets/images/week8/Visualization_13_1.png and /dev/null differ diff --git a/assets/images/week8/Visualization_15_1.png b/assets/images/week8/Visualization_15_1.png deleted file mode 100644 index d724688..0000000 Binary files a/assets/images/week8/Visualization_15_1.png and /dev/null differ diff --git a/assets/images/week8/Visualization_17_1.png b/assets/images/week8/Visualization_17_1.png deleted file mode 100644 index 2e026fb..0000000 Binary files a/assets/images/week8/Visualization_17_1.png and /dev/null differ diff --git a/assets/images/week8/Visualization_22_0.png b/assets/images/week8/Visualization_22_0.png deleted file mode 100644 index d724688..0000000 Binary files a/assets/images/week8/Visualization_22_0.png and /dev/null differ diff --git a/assets/images/week8/Visualization_24_1.png b/assets/images/week8/Visualization_24_1.png deleted file mode 100644 index a2c8da6..0000000 Binary files a/assets/images/week8/Visualization_24_1.png and /dev/null differ diff --git a/assets/images/week8/Visualization_29_1.png b/assets/images/week8/Visualization_29_1.png deleted file mode 100644 index 3b789c7..0000000 Binary files a/assets/images/week8/Visualization_29_1.png and /dev/null differ diff --git a/assets/images/week8/Visualization_32_1.png b/assets/images/week8/Visualization_32_1.png deleted file mode 100644 index f380703..0000000 Binary files a/assets/images/week8/Visualization_32_1.png and /dev/null differ diff --git a/assets/images/week8/Visualization_34_1.png b/assets/images/week8/Visualization_34_1.png deleted file mode 100644 index 63d3fa2..0000000 Binary files a/assets/images/week8/Visualization_34_1.png and /dev/null differ diff --git a/assets/images/week8/Visualization_36_1.png b/assets/images/week8/Visualization_36_1.png deleted file mode 100644 index 290574d..0000000 Binary files a/assets/images/week8/Visualization_36_1.png and /dev/null differ diff --git a/assets/images/week8/Visualization_38_1.png b/assets/images/week8/Visualization_38_1.png deleted file mode 100644 index 17d59ae..0000000 Binary files a/assets/images/week8/Visualization_38_1.png and /dev/null differ diff --git a/assets/images/week8/Visualization_40_1.png b/assets/images/week8/Visualization_40_1.png deleted file mode 100644 index 4561290..0000000 Binary files a/assets/images/week8/Visualization_40_1.png and /dev/null differ diff --git a/assets/images/week8/Visualization_43_0.png b/assets/images/week8/Visualization_43_0.png deleted file mode 100644 index 8bb8d0f..0000000 Binary files a/assets/images/week8/Visualization_43_0.png and /dev/null differ diff --git a/assets/images/week9/LinearRegression_4_1.jpeg b/assets/images/week9/LinearRegression_4_1.jpeg deleted file mode 100644 index c42e005..0000000 Binary files a/assets/images/week9/LinearRegression_4_1.jpeg and /dev/null differ diff --git a/assets/images/week9/LinearRegression_4_1.png b/assets/images/week9/LinearRegression_4_1.png deleted file mode 100644 index f34382a..0000000 Binary files a/assets/images/week9/LinearRegression_4_1.png and /dev/null differ diff --git a/assets/images/week9/LinearRegression_6_1.jpeg b/assets/images/week9/LinearRegression_6_1.jpeg deleted file mode 100644 index 895e1b1..0000000 Binary files a/assets/images/week9/LinearRegression_6_1.jpeg and /dev/null differ diff --git a/assets/images/week9/LinearRegression_6_1.png b/assets/images/week9/LinearRegression_6_1.png deleted file mode 100644 index 496302c..0000000 Binary files a/assets/images/week9/LinearRegression_6_1.png and /dev/null differ diff --git a/assets/images/week9/gradient_descent.jpeg b/assets/images/week9/gradient_descent.jpeg deleted file mode 100644 index 8e48630..0000000 Binary files a/assets/images/week9/gradient_descent.jpeg and /dev/null differ diff --git a/index.md b/index.md index 9c994f8..d1a8a14 100644 --- a/index.md +++ b/index.md @@ -2,75 +2,24 @@ layout: article show_edit_on_github: false license: false -title: 10.020 Data Driven World +title: Introduction to Computational Thinking and Problem Solving Using Python aside: toc: true --- -## About Data Driven World (DDW) +## About Introduction to Computational Thinking and Problem Solving Using Python (ICTPSP) -This course provides fundamentals for students with the necessary skills in a data driven world. The first half of the course focuses on providing students with algorithmic thinking and different paradigms of computation such as procedural, object-oriented design and state machine. The second half of the course focuses on a basic introduction to machine learning for categorical and continuous data. Students will be able to apply both algorithms and basic machine learning techniques to solve real-world problems driven by data and computation. +This book introduces some of the basic concept in computational thinking and problem solving by learning to program using Python programming language. It is known that learning to code is one of the most effective way to train people in computational thinking. There are many aspect of computational thinking such as abstraction, decomposition, pattern recoginitin and algorithmic thinking. A key part of this is problem solving. In this book, we apply a problem solving framework called PCDIT on various programming problems. This framework can be used in various situations where bottom up approach is needed. -![](/assets/images/home/course_overview.png) +One key aspect that differentiate computational thinking with other thinking skills such as mathematical thinking is that the solution to the problem can be carried out by a computing device or information-processing agent. Because of this such thinking process is closely coupled with the solution and how it is implemented. Though computational thinking can be taught without learning coding, in this book, we approach computational thinking in the context of learning programming using Python programming language. Since the programming language is used in the implementation of such solution, the solution and the thinking process is in a way affected by the choice of language up to a certain extent. -
- -![](/assets/images/home/DDW Concept Map-Big Topics and Period.drawio.png) ## Prerequisite -10.014 Computational Thinking for Design (Term 1) - -## Assessments - -| Components | Percentage | -| ----------------------------- | ---------- | -| Final Exam | 30 | -| Mid-Term Exam | 25 | -| 1D Projects | 15 | -| 2D Project | 10 | -| Cohort Sessions and Homeworks | 10 | -| Pre-Class Activities | 8 | -| Participation | 2 | - -For Audit students to be considered pass, they need to have above 80% scores for the following assessments: - -- 1D Projects -- Cohort Sessions and Homeworks -- Pre-Class Activities +This book assumes no prior knowledge of programming. Students are expected to be familiar on the use of basic computer operations. ## Learning Objectives By the end, students should be able to: -- Analyse different algorithmsโ€™s complexity in terms of computation time using Python computational model -- Identify recursive structure in a problem and implement its solution in Python -- Explain UML diagrams and design software using basic UML diagrams -- Apply appropriate data structure and implement them using object oriented design -- Implement algorithm to find coefficients for linear regression by minimizing its error -- Implement algorithm to classify categorical data using logistic regression for binary category and above -- Analyse and evaluate linear regression using mean square error and correlation coefficient -- Analyse and evaluate logistic regression using confusion matrix, its accuracy and recall -- Design state machine and implement it using object oriented paradigm -- Fix syntax errors and debug semantic errors using print statements - -See Detailed Learning Objectives for more details. - -## Text References - -Instructors will provide reading materials for each week. The references for this course include: - -- [How to Think Like a Computer Scientist](https://runestone.academy/runestone/books/published/thinkcspy/index.html) -- [Cormen, Thomas H., et al. Introduction to Algorithms, Third Edition, MIT Press, 2009. ProQuest Ebook Central](https://ebookcentral.proquest.com/lib/sutd/detail.action?docID=3339142) -- [James, Gareth et al. An Introduction to Statistical Learning: With Applications in R. Vol. 103. New York: Springer, 2013. Web](https://sutd.primo.exlibrisgroup.com/permalink/65SUTD_INST/1gbmki4/cdi_askewsholts_vlebooks_9781461471387) -- [Murphy, Kevin P.. Machine Learning : A Probabilistic Perspective, MIT Press, 2012. ProQuest Ebook Central](https://ebookcentral.proquest.com/lib/sutd/detail.action?docID=3339490) - -## Instructional Methods and Expectations - -The course will be run using a project-based and flipped-classroom strategy. Students are expected to do pre-class activities before coming to class. In-class hours are used to discuss and solve problems as well as to do projects. Each week there are mini-projects related to the topics just introduced in that week and it culminates in one open project at the end. - -Students are expected to do their pre-reading and homework on their own while discussing the cohort sessions and projects with the instructors in class. There will be hands-on programming activities for all cohort sessions. - -## Lesson Format - -![](/assets/images/home/lesson_format.png) +See Detailed Learning Objectives for more details. \ No newline at end of file