# ---
# jupyter:
#   jupytext:
#     text_representation:
#       extension: .py
#       format_name: light
#       format_version: '1.5'
#       jupytext_version: 1.15.2
#   kernelspec:
#     display_name: Python 3
#     name: python3
# ---

# + [markdown] id="view-in-github" colab_type="text"
# <a href="https://colab.research.google.com/github/sultanalnahian/Coursera_MachineLearning-Regression/blob/master/content/00_prerequisites/00_introduction_to_python/colab.ipynb" target="_parent"><img src="https://colab.research.google.com/assets/colab-badge.svg" alt="Open In Colab"/></a>

# + [markdown] id="copyright"
# #### Copyright 2020 Google LLC.

# + id="vUL-MxtcowtQ"
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

# + id="RPzkgaO1IzxD"
"TEST"

# + [markdown] id="ZutSxI49o8Jh"
# # Introduction to Python

# + [markdown] id="ZutSxI49o8J5"
# Python is the most common language used for machine learning. It is an approachable yet versatile language that's used for a variety of applications.
#
# It can take years to learn all the intricacies of Python, but luckily you can learn enough Python to become proficient in machine learning in a much shorter period of time.
#
# This Colab is a quick introduction to the core attributes of Python that you'll need to know to get started. This is only a brief peek into the parts of the language that you'll commonly encounter as a data scientist. As you progress through this course, we'll introduce you to the Python concepts you'll need along the way.
#
# If you already know Python, this lesson should be a quick refresher. You might be able to simply skip to the exercises at the bottom of the document.
#
# If you know another programming language, you will want to pay close attention because Python is markedly different than most popular languages in use today. If you are new to programming, welcome! Hopefully this lesson will give you the tools you need to get started with data science.

# + [markdown] id="nitMt74ZpjSN"
# ## Variables
#  
# One of the most important notions in any programming language is the **variable**. Variables are how we store data we want to use later.
#  
# When we have an expression like `2 + 3`, Python calculates the value of `5` and then forgets that the value ever existed. We need some way to store the values of expressions so that they can be used later in the program. To do that, we use variables.
#  
# In the example below, the value of `2 + 3` is stored in the variable `x`.

# + id="rNMTYmdOuS1t"
x = 2 + 3
x

# + [markdown] id="eixDMWWW1oDo"
# Variables store values, and those values can be used in expressions.

# + id="mgUEjQbk1yEV"
x = 2
y = 3
x + y

# + [markdown] id="jC17KacSEvdZ"
# Variable value can also be changed, hence the name *variable*.

# + id="2wYs4zbuE6rA"
x = 123
x = 456
x

# + [markdown] id="mT_0fsbuFses"
# There are very few rules regarding what you can name a variable. The first character needs to be an alphabetic character. After the first character, any alphanumeric character can be used. Underscores are also okay to use anywhere in a variable name, but stay away from naming a variable with two underscores at the beginning, since Python uses leading double-underscores for internal things. (This will be seen in a later lesson.)
#
# Here are a few valid variable names:

# + id="_EVPT2D-GbUp"
number = 1
my_number = 2
YourNumber = 3
_the_number_four = 4
n5 = 5
NUMBER = 6

number + my_number + YourNumber + _the_number_four + n5 + NUMBER

# + [markdown] id="6LYdiJ4DGyy5"
# Notice that `number` and `NUMBER` are different variables. Case matters.
#
# Although Python will accept other styles, it is common convention to name constants in all caps (e.g. `THE_NUMBER`) and variables using lower_with_underscore syntax (e.g. `a_number`). See the [Python Style Guide](https://www.python.org/dev/peps/pep-0008/#naming-conventions) for more on naming conventions. Adhere to the guide when you can. It will help others understand your code and it will train you to be able to read other programmers' Python code.

# + [markdown] id="n6232TxSHCdA"
# Naming variables is an important aspect of computer programming. Variables serve as a form of documentation within your code. Good names will help your teammates and your future self understand what your code is doing when they are trying to modify it. Take some time to think about your variable names as you create new variables.
#  
# It is also important to keep your variable naming style consistent. Don't use different naming styles (such as `this_variable` and `thisVariable`) in the same file/package unless you have a valid reason to do so.

# + [markdown] id="EpzRj0xf7_Qf"
# ## Printing and Strings

# + [markdown] id="z953Qlkk7RvE"
# ### Strings
#
# The **string** data type can contain zero or more characters. In order for Python to know that a string is a value and not part of the code, strings have to be wrapped in quotes.
#
# Some example strings are:

# + id="bkdwYrWTDdSu"
"Python is a "
'useful programming language'

# + [markdown] id="oG8rzEjCIrFN"
# Single and double-quotes are interchangeable, and both occur frequently. In general, try to pick one style of quotation marks and stick with it, unless you need to use that type of quote in a string.
#
# The following string requires the `'` character within the string, so it makes sense to use `"` as the quotation marks.

# + id="7i7kcmjWJOiq"
"I've been learning Python"

# + [markdown] id="s23P6KyYJXmB"
# But what if you need to use both `'` and `"` in the same string? In that case you can *escape* the embedded quote using the `\` character. The `\` tells Python to read the character `'` as it is, and not as the end of the quotation.

# + id="4ko6NWgQJet7"
'They\'ve had a "good" trip'

# + [markdown] id="2oQvnltzJ0ii"
# You probably noticed that the escape character, `\`,appears in the output. This is just a side effect of how Colab is printing the string. We'll learn a cleaner method of printing a string soon.

# + [markdown] id="S57KwUN6KYlc"
# The triple-quote (`"""` or `'''`) is another type of quote that you can use to create a string. The triple-quote allows you to have a multi-line string. It is often used when writing documentation directly in your code.

# + id="kENH5-B_Ky3j"
"""This is a string
surrounded by three double-quotes
and spanning multiple lines.
"""

'''This is a string
surrounded by three single-quotes
and spanning multiple lines.
'''

# + [markdown] id="W2DnBnAZK2Hh"
# You can see in the output that the string shows up on one line with `\n` added where the line breaks were. `\n` is a special escape sequence that means "line feed", which is typewriter-speak for moving to the next line. `\t` is another common escape sequence that represents a tab. `\\` adds a backslash `\` to the string.

# + [markdown] id="CqrJdWZMLzYt"
# Strings can be stored in variables. The `+` operator also works on strings, by concatenating them together.

# + id="ATDV6CX_MDD6"
s1 = "The Python "
s2 = "programming"
s3 = " language is easy to learn"
s1 + s2 + s3

# + [markdown] id="f6yO48xYMQ8h"
# The `*` operator also works with strings, resulting in the string repeated multiple times. However, other arithmetic operators such as `-` and `/` don't work with strings.

# + id="tFEZNL-UMMuS"
'ABC ' * 5

# + [markdown] id="bmqwHLl6Mng8"
# Python has a handy built-in way to find the *length* of a string, which is the number of characters in the string.

# + id="L-aLHVrANalL"
len("pneumonoultramicroscopicsilicovolcanoconiosis")

# + [markdown] id="dG3TxrOxOEry"
# If you need to extract a specific character from a string, you can specify that character by *indexing* it. Notice that in Python, the first character of a string is at index 0. Most popular programming languages start counting at 0.

# + id="fym2JmR4O2O1"
"abcdefghijklmnopqrstuvwxyz"[1]

# + [markdown] id="mdsjwVSBO_kN"
# You can also extract a *slice* of a string. A slice is a portion of a string referenced by the starting and ending index. The starting index is inclusive, and the ending index is exclusive. The slice below returns a four-character string.

# + id="Catcl26sPPpK"
alphabet = "abcdefghijklmnopqrstuvwxyz"
alphabet[1:5]

# + [markdown] id="bv18nj19P0y6"
# Slices have some handy shortcuts if you want to start at the beginning or go all the way to the end of a string.

# + id="s4CTYF86P_8c"
alphabet[:3]

# + id="8PNkhfs-QHPg"
alphabet[23:]

# + [markdown] id="zMRmvX9qQkON"
# Strings are *objects* in Python. We won't get into the details of what an object is in this tutorial, other than to mention that objects can have functions and methods called on them using **dot notation**. Below is an example object call on the `alphabet` string, that converts the entire string to uppercase. Notice that the function is called `alphabet.upper()` instead of `upper(alphabet)`, as we saw with the `len()` function.

# + id="zQCvFzGUQg4r"
alphabet.upper()

# + [markdown] id="WXBznfKiRcsb"
# More information on string functionality in Python can be found in the [Python string](https://docs.python.org/3.7/library/string.html) documentation.

# + [markdown] id="7TXyYyUBv77C"
# ### Printing
#
# So far, we have relied on Colab to print out the data that we have been working with. This works fine for simple examples, but it doesn't work if you want to print multiple times in a code snippet.
#
# The **`print`** function allows you to print any data structure to the screen.

# + id="UeZabHvqv769"
my_variable = "I'm a variable"
print(my_variable)
print("Hello class!")
print(12345)
print(123.45)
print(['a', 3, 'element list'])
print(('a', 3, 'element tuple'))
print({"my": "dictionary"})

# + [markdown] id="1wopo_J1v766"
# You'll notice that `print` adds a new line to the output every time it is called. We can add an `end=''` argument to the `print` to take away the new line.

# + id="mrmKTVDXv760"
print("The magic number is ", end='')
print(42)

# + [markdown] id="IkXnWJ3Pv76y"
# There is another Python feature that, though not strictly just for printing, is commonly used when printing: **text formatting**.
#
# Text formatting allows you to print multiple values in a single `print` statement and to mix strings and numbers.

# + id="qDCoQSCFv76s"
print("%s says that the numbers %d and %f sum to %f" % ("Bob", 3, 5.1, 3 + 5.1))

# + [markdown] id="1Nnhnon_v76p"
# There is quite a bit going on in the code above, so let's break it down into pieces:
#  
# * The first string we see in the `print` call is `"%s says that the numbers %d and %f sum to %f"`. This string contains `%s`, `%d`, and `%f` placeholders. These placeholders tell Python that we expect a string (`%s`), integer (`%d`), or floating-point number (`%f`) that we'll put in the string. (A floating-point number is any non-integer number.)
#  
# * Next we see a percentage sign `%` after the string. This is the string formatting operator. It comes after a string and precedes the tuple containing the data that will populate the placeholders in the string.
#  
# * Finally comes the tuple containing the data that will populate the placeholders. The data in the tuple should be in the same order that the placeholders appear in the string. For example, the second placeholder is `%d`, and the second item in the tuple is the integer `3`.

# + [markdown] id="PihhIEJSv76m"
# There is a more modern and object-oriented formatting function that can be used to achieve the same goal. Notice the placeholders in the string passed to the `print` function are all curly braces. Also, the floating-point values are truncated to the appropriate number of decimal points in the output.

# + id="44loLjJZv76W"
print("{} says that the numbers {} and {} sum to {}".format("Bob", 3, 5.1, 3 + 5.1))

# + [markdown] id="n0pk26GVMdAf"
# When the values you want to print are saved as variables, you can also pass them in the `print` function by typing `f` before the quotation marks and putting the variable names in the curly braces.

# + id="iSrBG3s8MtJz"
name = "Bob"
value1 = 3
value2 = 5.1
print(f"{name} says that the numbers {value1} and {value2} sum to {value1 + value2}")

# + [markdown] id="ED0MmXO9Ytkl"
# #### Exercise 1

# + cellView="form" id="AGaNNvtlHtA6"
#@markdown Run this cell before starting the exercise.
#@markdown It initializes the values of some hidden variables.

name = "Alex"
favorite_food = "chocolate"

# + [markdown] id="OvfsalseMDaq"
# We have hidden the values of variables `name` and `favorite_food` in the cell above. Use a `print` statement to find out their values.

# + [markdown] id="Pb4rwUm6qXA4"
# #####**Student Solution**

# + id="bOsixW2-MCfK"
# Use a print statement to show the values of the hidden variables 
# "name" and "favorite_food".
print("My name is ____. I love eating ____.")

# + [markdown] id="d4veL73wGHpO"
# ---

# + [markdown] id="I7KYyR11pR7G"
# ## Basic Data Types
#
# This section will introduce you to some of the most common data types that you'll encounter. Data types are the foundational building blocks for any Python code.

# + [markdown] id="4ee4By1ppGSW"
# ### Integers
#
# Integers are just whole numbers. This includes positive whole numbers (1, 2, 3, ...), 0, and negative whole numbers (-1, -2, -3, ...). The cell below demonstrates the addition of two integers.

# + id="Y6ENY1AaCe-R"
42+8

# + [markdown] id="xa2r21GQDyX_"
# Python can do all the common operations you are familiar with.

# + [markdown] id="JL759v61C8WB"
# **Subtraction**

# + id="bmpIeEdCC0qy"
4-2

# + [markdown] id="MRcuhLf3DBYK"
# **Multiplication**

# + id="rFTc9fNiDLBO"
2*3

# + [markdown] id="MsSSofZeDUMQ"
# **Exponents**

# + id="CZrY6jcKTZ-g"
2**3

# + [markdown] id="iQ7VTcjQTwL0"
# **Division** is a bit more complicated. If a number doesn't divide evenly, such as `13 / 5`, we get what is called a floating-point number. We will talk about floating-points in a bit, but for now, we can use `//` when dividing, also known as "floor division". This operator removes any decimal remainder (e.g. 14.1 and 14.9 both become 14). To find this remainder, we can use a modulo operator, `%`.

# + id="5YSFvvusWbM5"
quotient = 14 // 4
remainder = 14 % 4

print(f"Quotient: {quotient}, Remainder: {remainder}")

# + [markdown] id="X2TdlbVkD8KL"
# Multiple mathematical operations can be combined, as below:

# + id="WeEzppbcXmcu"
7 ** 7 + 7 * 7 - 7 * 7 // 7

# + [markdown] id="soWd9ICNXksr"
# The code above can be a bit difficult to read. How does Python know which numbers to process first?
#
# Python enforces an order of precedence where operations like taking the exponent come before multiplication and division, which come before addition and subtraction. (See this guide to [order of operations](https://www.mathsisfun.com/operation-order-pemdas.html).) There are actually more operators than what we've seen so far and it can be hard to remember the order of operations, so when in doubt you can wrap parentheses around operations to make things clearer.

# + id="WUHW2o4BEeeg"
(7 ** 7) + (7 * 7) - ((7 * 7) // 7)

# + [markdown] id="kC10wZPyjLyC"
# You can change the order of the operations from the standard order by wrapping different expressions in parentheses. Run the code snippets above and below, and notice the different results despite having the same numbers and operators.

# + id="BZpQixwpFjFZ"
(7 ** (7 + 7)) * 7 - (7 * (7 // 7))

# + [markdown] id="fqQQ2FVTN0__"
# #### Exercise 2

# + [markdown] id="IAVzd_nmGRK2"
# **Four Fours Problem**
#
# Don't spend more than 15 minutes on this problem, but give it a few tries.
#
# Change the operations and add parenthesis as necessary to try and come up with as many numbers between 1 and 42 as you can.

# + [markdown] id="KoJgYJnAFv8G"
# ##### **Student Solution**

# + id="UiTK_43yGbRL"
4 + 4 + 4 + 4

# + [markdown] id="_MuiR_mXGCeH"
# ---

# + [markdown] id="sN83BIYvMlbJ"
# ### Floating-Point Numbers

# + [markdown] id="hE54jmP2LS1p"
# It might seem complicated, but Python has a few ways of representing numbers. You have already seen the integer data type. This section is about floating-point numbers.
#
# The different data types representing numbers have a different place in the world of computing. In some languages, it is quite challenging to move from one type of number to another (e.g. from an integer to a floating-point). In Python, the mixing of integers and floating-point numbers is more fluid. This is great most of the time, but can be problematic at times. It is important to be aware, especially when doing division, when you are working with floating-point numbers or integers.

# + [markdown] id="L-axSTVAfdPX"
# Earlier we looked at floor division using `//`. The cell below uses regular division using `/`.

# + id="5Xl53vhnDP04"
50 / 10

# + [markdown] id="StSlr3ENHczu"
# In this case we can see that there is a decimal point (`.`) in the output. This is generally how we'll know that we're working with floating-point numbers. 
#
# You can force operations to be floating-point operations by including one floating-point number in the equation. Note the difference between the following two numbers.

# + id="HnX7JoF7IjL-"
print(2 + 3)
print(2.0 + 3)

# + [markdown] id="gcMocTbXQu4z"
# The cell below demonstrates a standard division of two floating-point values.

# + id="BI-TEL_ARVvM"
322.231 / 0.03

# + [markdown] id="DAdljqYvRfif"
# The result `10741.033333333333` is clearly a floating-point number, with a decimal portion of `.033333333333`. Here is what happens when we apply floor division to two floating-point values:

# + id="RMUAi_QgRl7V"
322.231 // 0.03

# + [markdown] id="eGF6o-DSRp8Q"
# Note that the result `10741.0` has a decimal portion of `.0` now, but the decimal point indicates that it is a floating-point value.

# + [markdown] id="uzHSFAAzqKgC"
# Python has a very useful package with more advanced math tools. To import this package, all you need to do is add the following line to your code.

# + id="qXY1A9pPqdzI"
import math

# + [markdown] id="9YCJ7qnbqjFH"
# This package gives us access to a bunch of useful things, such as trigonometric functions like sines and cosines, and mathematical constants like $\pi$ and $e$. To use them, we just type `math.` followed by the specific function we want to use. In Colab, if you've already imported the library, typing `math.` should show a list of available functions.
#
# If you get an error running the following cell, make sure you run the cell above which imports the `math` package.

# + id="RWGjupl9rIho"
math.pi

# + [markdown] id="Qg3Om6uqSHYq"
# Here, `math.pi` ($\pi$) is represented as a floating-point number. Since $\pi$ is an irrational number, its value cannot be represented with a decimal, so the value of `math.pi` is not *exactly* the value of $\pi$. However for most use cases, it is close enough.

# + [markdown] id="_8UyzFRlPTZe"
# While computers cannot generate truly random numbers, computer scientists have come up with some very clever ways to get pseudo-random numbers. These numbers aren't perfectly random, but are usually close enough such that it doesn't matter. In Python, it is very easy to get a variety of pseudo-random values using the `random` library. It is imported using the same syntax as seen earlier to import the `math` library.

# + id="_cTqHwQRQ0kD"
import random

# + [markdown] id="XGe7XRfHRfqA"
# As we saw with the `math` library, any time we want to use a function from the library, we need to preface the function name with `random.`. To get a random floating-point value uniformly chosen between 0 and 10, we can type the following command.
#
# If you get an error running the following cell, make sure you run the cell above which imports the `math` package.

# + id="BWoP6GmDVseg"
random.uniform(0, 10)

# + [markdown] id="w9_mB1ZNar_K"
# Alternatively, we can get a random value between 0 and 1 using `random.random()` and simply multiply it by 10 to achieve the same thing. There are cases where one method might be preferable over the other, so it is useful to know them both.

# + id="hz9bF9bRbISv"
random.random()*10

# + [markdown] id="BRrRfDjdbR_A"
# There are a lot more kinds of random numbers that we can use, but we'll talk about that more a bit later.

# + [markdown] id="ZbyMX1f-UFxj"
# Don't be intimidated by the differences and interactions between floating-point numbers and integers. Most of the time you won't need to be concerned about it. However, if you see surprising results when doing math in Python, be sure to double-check the data types of the numbers in your operations.

# + [markdown] id="xhWi4A7G9sxP"
# ### Booleans
#
# The boolean is another core data type in Python. Booleans, often abbreviated to "bools", can be thought of as switches, with an "on" and an "off". Bools are represented simply by **True** and **False**.

# + id="8kK7X0KTh9cT"
True

# + id="yb8R0X5piBmy"
False

# + [markdown] id="GjxWzrHGiLvS"
# Bools are often combined with logical operators such as **`and`** and **`or`**. Suppose you have two boolean variables, `a` and `b`.
#
# - `a and b` is `True` if and only if both `a` and `b` are `True`, otherwise it is `False`.
# - `a or b` is `True` if and only if at least one of `a` or `b` is `True`, otherwise it is `False`.

# + id="ku0QRmgOiwqi"
True and True

# + id="n5jaK8mAi1kr"
True and False

# + id="1QsUSB_HjFQi"
False and True

# + id="kbFv2tF2i5uC"
False and False

# + id="TvaiLtfIjCHK"
True or True

# + id="fNC2DXODjO1j"
True or False

# + id="E4Vsi4yyjRIU"
False or True

# + id="dPHZfa0SjTpi"
False or False

# + [markdown] id="CTFYOYm-jl8s"
# These expressions of *truthiness* can be expanded beyond two operands:

# + id="dHDN9rlHju1K"
False and True or True and False or True

# + [markdown] id="EWds8NZcjzLm"
# Just like with numbers, you can change the order in which the boolean operators are conducted, with parentheses.

# + id="bmNET_M0j8n6"
False and (True or True and False or True)

# + [markdown] id="a6qGkJawkKOy"
# Truthiness can be flipped with the **`not`** operator:

# + id="0QuMqZr-kNuq"
not True

# + id="ktrkaFPvkP3C"
not False

# + [markdown] id="MAMZiWVKkRyj"
# In most cases, you won't be working directly with **`True`** and  **`False`**. Most of the time, these values will be returned from other expressions. Take for instance the greater than ($>$), less than ($<$), greater than or equal to ($\geq$), and less than or equal to ($\leq$) expressions below:

# + id="kvrU-MbntJxF"
2 > 1

# + id="jC3Tu27etMz9"
2 < 1

# + id="dyAbguAZtStV"
1 >= 1

# + id="lV4LB8yltXX0"
2 <= 1

# + [markdown] id="iM0WzNIzt2e6"
# There are also checks for equality and inequality:

# + id="aWh_nf2Ct6BF"
1 == 2

# + id="DqD57oCjt9Fc"
1 != 2

# + [markdown] id="bPxW9UbyuOM8"
# Why is "equals" represented as `==` instead of just `=`? As you saw earlier with variables, you can use `=` to assign a value to a variable. Therefore, Python needs a different symbol to check for equality. If you want to assign the variable `a` to the value 3, you can use `a = 3`. Now, if you want to check if it is equal to 3, you can use `a == 3`.

# + id="LvdgS_NdNfah"
a = 3
print(a)
print(a == 3)
print(a == 2)
a = 2
print(a)
print(a == 2)

# + [markdown] id="xCtBPbjZteEN"
# You can combine the logical **`and`**, **`or`**, and **`not`** expressions and the **`>`**, **`>=`**, **`<`**, **`<=`**, **`==`**, **`!=`** expressions. Parentheses are again used to change the order of operation evaluation.

# + id="31SI26qkutT9"
(1 < 2) and (3 == 3) or ((4 > 1) and (not 1 < 2))

# + [markdown] id="v7Ia_42jhGza"
#
# We can also use the **`>`**, **`>=`**, **`<`**, **`<=`**, **`==`**, **`!=`** expressions with two strings to determine whether strings are in alphabetical order.

# + id="LKzFY0yohd2V"
'apple' < 'banana'

# + [markdown] id="OQ0u4X2Mhs3I"
# Of note is that capital letters are sorted before lowercase e.g. `'A' < 'a'`.

# + id="SlrWHza2Of1e"
'Apple' < 'apple'

# + [markdown] id="l7ev4qLUWRT6"
# #### Exercise 3

# + cellView="form" id="QFOF5-zrWRUB"
#@markdown Run this cell before starting the exercise.
#@markdown It initializes the values of some hidden variables.

favorite_letter = 'K'
favorite_number = 42

# + [markdown] id="y8vtK6YbWRUI"
# We've hidden the values of variables `favorite_letter` and `favorite_number` (an integer between 0 and 100). Use boolean expressions to find the values of the letter and the number.
#

# + [markdown] id="81jAhKnrWRUI"
# ##### **Student Solution**

# + id="PRR6zg7CWRUJ"
# Edit the expression below until you discover the hidden letter
favorite_letter == 'A'

# + id="chMvU3wSWRUM"
# Edit the expression below until you discover the hidden number
favorite_number <= 100

# + [markdown] id="4M-iD5qGWRUQ"
# ---

# + [markdown] id="j0CX7jke7lqR"
# ## Conditional Decisions
#
# One of the most common patterns in computer science and data science is the `if` statement. You can use `if` to check if a condition is met, and do different things based on whether it is met or not. The `if` statement looks at a boolean value and if that value is `True`, runs some code.

# + id="zuF7LVOS7FhQ"
if 1 > 3:
  print("One is greater than three")

if 1 < 3:
  print("One is less than three")

# + [markdown] id="Cv30H9ZrB1cI"
# Only the second `print` statement is run, since it is not `True` that `1 > 3` but it is `True` that `1 < 3`.
#  
# Notice that the two `print` statements are indented beneath each `if` statement. This isn't by accident. Python creates "blocks" of code using the code's indentation level. This indentation can be done with tabs or with spaces, but it must be consistent throughout your code file.
#  
#  ```
# block 1
#   block 1.1
#     block 1.1.1
#  
#     block 1.1.1
#  
#   block 1.1
# ``` 
#  
# The code below shows blocks in action.

# + id="gBDfIw-bErkp"
if False:
  print("This shouldn't print.")
print("But this will always print.")

# + [markdown] id="RXYHdR-JFiET"
# In many situations, you will want to run some code if a condition is met, and some different code when it is not. For this, you can use `else`.

# + id="5SsJLEarF3kZ"
if 1 > 3:
  print("Math as we know it is broken!")
else:
  print("Everything looks normal.")

# + [markdown] id="HyMZNuRjF_J6"
# You might also want to check many if conditions and only execute the code if one condition passes. For this, you can use the `elif` clause (short for "else if").
#
# For example, these would be useful if we wanted to do a simple rock, paper, scissors game.

# + id="nTi0_DBTGJsK"
# Choose a random option from the list for the computer player.
import random
computer = random.choice(["rock", "paper", "scissors"]) 

my_choice = "paper" # Feel free to change this.

print(f"You chose {my_choice}!")
print(f"The computer chose {computer}!")

if my_choice == computer:
  print("Draw! Go again!")

elif my_choice == "rock" and computer == "paper":
  print("The computer wins. Try again?")

elif my_choice == "rock" and computer == "scissors":
  print("You smashed the computer's scissors!")

elif my_choice == "paper" and computer == "rock":
  print("You wrapped up the computer's rock!")

elif my_choice == "paper" and computer == "scissors":
  print("The computer wins. Try again?")

elif my_choice == "scissors" and computer == "rock":
  print("The computer wins. Try again?")

elif my_choice == "scissors" and computer == "paper":
  print("You sliced up the computer's paper!")

# + [markdown] id="jlkFACKQoKyL"
# ## Basic Data Structures

# + [markdown] id="4aeUPXVUpoVb"
# ### Lists

# + [markdown] id="hvrg40h6Sacg"
# So far, the data types we've seen can be thought of as singular entities. We've seen:
# - strings
# - integers
# - floating-points
# - booleans
#
# Often, you'll find yourself needing to work with multiple data elements together. There are several options for organizing a collection of data into a data-structure. One option is to use a list.
#
# A list is just a list of multiple values.

# + id="0c7EWoTkTBXc"
[9, 8, 7, 6, 5]

# + [markdown] id="ffB5qW6iTEkp"
# The values in a list don't need to have the same data type.

# + id="d9UaR3X8THH-"
[True, "Shark!", 3.4, False, 6]

# + [markdown] id="RaFVKpWYTMoV"
# You can assign a list to a variable.

# + id="GxoedcOvTdBb"
my_list = [True, "Shark!", 3.4, False, 6]
my_list

# + [markdown] id="UzZ3LCjiUnj-"
# You can also index a list and take slices from it, just like you can from a string. Conceptually you can think of "a string" to be a sequence of characters similar to a list.

# + id="1FGtpudFU9FV"
print(my_list[3])
print(my_list[3:])

# + [markdown] id="tLH5kW4pXodm"
# Indexing can also be used to selectively replace items in a list.

# + id="aVhNNk3EXtAX"
print(my_list)
my_list[1] = "Wolf!"
print(my_list)

# + [markdown] id="rXjH5eZ0VDGo"
# Lists have other interesting features. For example, there is an in-built method to sort a list.

# + id="NsDXdr4OVINd"
number_list = [4, 2, 7, 9 ,3, 5, 3, 2, 9]
number_list.sort()
number_list

# + [markdown] id="0VK9yibsdHpl"
# You can append an item to a list using `append()`.

# + id="rUs-ifRpdLmY"
letter_list = ["a", "b", "c"]
letter_list.append("d")
letter_list

# + [markdown] id="NBMEjCAmd1C3"
# You can append multiple items to a list using `extend()`.

# + id="YOMz3t0Fd5E_"
letter_list.extend(["e", "f", "g"])
letter_list

# + [markdown] id="lZTvQem9VUQk"
# You can even have lists within lists!

# + id="oLhCs9bUVXtD"
["List 1", ["List 2", 3, 4], False]

# + [markdown] id="z1igsdg_Vfaa"
# Lists-of-lists come in really handy, especially in data science since much of the data that you'll work with will be in a tabular format. In these cases, the internal lists are typically the same size. For example, you might have a list of data points about a customer, such as their age, income, and the amount they spent at your company last month.

# + id="vCbbTrx8WCRb"
customers = [
    ["C0", 42, 56000, 12.30],
    ["C1", 19, 15000, 43.21],
    ["C2", 35, 123000, 45.67],
]
customers

# + [markdown] id="RJjfjKPXW3_b"
# You can use multiple indexing to get data out of a nested list. In the example below, we pull out the income of our second customer.

# + id="m3tVFhSpXOxU"
customers[1][2]

# + [markdown] id="np3pCakUWqzH"
# We will explore lists more deeply and other data structures in future tutorials.

# + [markdown] id="DaHEjvmwpqzT"
# ### Tuples
#
# Tuples look and feel a whole lot like lists in Python. They can contain a sequence of data of any type, including lists and other tuples. The primary difference between lists and tuples is that you can't modify a tuple like you can a list.
#
# Before we get too deep into immutability (whether you can change an object's value), let's take a look at a tuple.

# + id="s2wbBKB2YYr9"
my_tuple = (1, "dog", 3.987, False, ["a", "list", "inside", 1.0, "tuple"])
my_tuple

# + [markdown] id="5gTQ6Gx6Yp1g"
# The visible difference between a list and a tuple is that we create a tuple with parentheses instead of square brackets.
#
# You can index a tuple and take a slice from a tuple just like you can from a list. 
#
# The only difference is that you can't change the values inside a tuple, like you can with a list. This is useful because Python can perform some optimizations when it knows a data structure can't change. This gives tuples a few powerful properties that lists don't have. We'll take a peek at one of these properties now, and we'll also learn more later in this tutorial.
#
# We will use a property of a tuple to swap the values of two variables. In most languages, you need three variables to swap the value of two variables. Here is an example.

# + id="0Ufn_-6vZfws"
var1 = "Python"
var2 = "Perl"

tmp = var1
var1 = var2
var2 = tmp

var1, var2

# + [markdown] id="e86S6sL9ZzwO"
# We had to introduce the `tmp` variable to perform the swap, and needed three lines of code. With tuples, we can do this more cleanly.
#
# **Note:** You might have noticed that when we put `var1, var2` at the bottom of the last code section a tuple was printed out. Any sequence of variables separated by commas in Python automatically creates a tuple.

# + id="kmbYg8KdaVNW"
var1 = "Perl"
var2 = "Python"

(var1, var2) = (var2, var1)

var1, var2

# + [markdown] id="UMvM-x4baz6o"
# As you can see, swapping variables using tuples is much easier to read and less error-prone than having to use three variables. It uses the property that the values in a tuple are **immutable**.

# + [markdown] id="OUI-5GqHbB5L"
# Tuples come up everywhere when programming in Python. Sometimes you won't even realize that you are working with a tuple, since they are so integrated with the language.

# + [markdown] id="AM2MY7EPpsS5"
# ### Dictionaries
#
# Dictionaries are another fundamental data structure in Python. If you have experience with other programming languages, you might have encountered a similar data structure with a different name such as a map, a hashmap, or a hashtable.
#
# Dictionaries contain key/value pairs. The reason this data structure is called a dictionary is because you can "look up" keys and find the corresponding value, just like you can look up a word in the Oxford Dictionary and find its definitions.
#
# Let's take a look at some code that creates a dictionary and accesses a value in the dictionary by key.

# + id="GkvFhLkguhln"
my_dictionary = {
    "pet": "cat",
    "car": "Tesla",
    "lodging": "apartment",
}

my_dictionary["pet"]

# + [markdown] id="jhS92kCxu2N-"
# Notice that we use the same *indexing* notation that should be familiar to you from strings, lists, and tuples. However, instead of a numeric index, the look up is done by key.
#
# A key can be any non-mutable data value. Keys can be numbers, strings, and even tuples. You can't use a dictionary or list as a key, but you can use them as values.
#
# The data types of the keys do not need to be the same.

# + id="F7H45vAkvTc-"
the_dictionary = {
    57: "the sneaky fox",
    "many things": [1, "little list", " of ", 5.0, "things"],
    (8, "ocho"): "Hi there",
    "KEY_ONE": {
        "a": "dictionary",
        "as a": "value"
    },
}

the_dictionary[(8, "ocho")]

# + [markdown] id="Ljuhxa7sv0Ze"
# The dictionary above is much more unstructured than dictionaries that you'll typically encounter in practice. However, it illustrates the broad range of key types and value types that a dictionary can store.
#
# You can also index many levels down in a dictionary. For example, in `the_dictionary` above, there is a sub-dictionary at the `KEY_ONE` key. Let's pull something out of this dictionary within a dictionary.

# + id="-9d8N3kxwgp_"
the_dictionary["KEY_ONE"]['as a']

# + [markdown] id="5faIdUYRwrZp"
# We can also use indexing to access the values of the list that is the value in `the_dictionary` for the key `"many things"`.

# + id="8QI8rJKMwtyH"
the_dictionary["many things"][1]

# + [markdown] id="gBwk3lyzxFlg"
# Dictionaries, lists, tuples, and other data structures can contain as much nesting as you need.

# + [markdown] id="FyKt13WvxO0K"
# Dictionaries store their values by key. Only one value can exist per key, so if you write a new value to a key, the old value goes away.

# + id="XjvSiO5pxaQb"
my_dictionary = {
    "k1": "name",
    "k2": "age"
}

my_dictionary["k1"] = "surname"

my_dictionary

# + [markdown] id="dySuvUTfxn-h"
# You can add entries to a dictionary by assigning them to a key.

# + id="KbMBUHKgxtWl"
my_dictionary["k3"] = "rank"

my_dictionary

# + [markdown] id="BAi6X1T9xznZ"
# And you can remove entries from a dictionary using the **`del`** operator.

# + id="DzeaUA_lx7Vh"
del my_dictionary["k2"]

my_dictionary

# + [markdown] id="DF9ipqh2yDwM"
# To see if a key exists in a dictionary, you can use the **`in`** operator. Notice that it returns a boolean value.

# + id="7-M5xn9DyJcS"
"k2" in my_dictionary

# + [markdown] id="mGe0UEq4yUaI"
# It is advisable to check if a key exists in a dictionary before trying to index that key. If you try to access a key that doesn't exist using square brackets, your program will throw an error and possibly crash.
#
# There is also a safer **`get`** method on the dictionary object. You provide `get` with a key and a default value to return if the key isn't present.

# + id="gm5yy399y2Hk"
my_dictionary.get("k2", "There is no 'k2' key value")

# + [markdown] id="N4eyTWNPy7Fc"
# For more on dictionaries, check out the [official Python dictionary documentation](https://docs.python.org/3/tutorial/datastructures.html#dictionaries).

# + [markdown] id="KiF3xtSI40Tw"
# We've learned about the most fundamental data structures in Python:
# - numbers
# - booleans
# - lists
# - tuples
# - dictionaries
#
# We've learned how to store data in variables and how to change data in variables, dictionaries and lists. Each of these data types have more functionality than we have gone over in this tutorial, so please take some time to get a broader idea of what can be done with these data types in Python.
#  
# There are also many data types that we did not cover. As we encounter the need for other types of data in our study of machine learning and data science, we will introduce and explain some of them.

# + [markdown] id="qFPc3YgQ7ocD"
# ## `for` Loops
#
# The `for` loop is a powerful tool that lets us look at every item in a data structure in order and perform operations on each element.

# + id="sk8Aw0yZJCUf"
my_list = ['a', 'b', 'c']

for item in my_list:
  print(item)

# + [markdown] id="dirxZstuJQnI"
# As you can see, the `for` loop executes `print` three times, once for each item in the list.
#
# The `for` loop works for tuples too.

# + id="p4Q9TFIEJZdK"
my_tuple = (5, 3, 1, -1, -3, -5)
for x in my_tuple:
  print(x)

# + [markdown] id="-VfTc3TBJk5r"
# Dictionaries are a little more interesting. By default, the loop works by indexing the keys.

# + id="gSZist0eJrVY"
my_dictionary = {
    "first_name": "Jane",
    "last_name": "Doe",
    "title": "Dr."
}

for k in my_dictionary:
  print(f"{k}: {my_dictionary[k]}")

# + [markdown] id="Ir9HX9bSJ827"
# If only the dictionary's values are of concern to you, it is possible to ask the dictionary to return its values by using the `values()` method.

# + id="ktslT2NVKD3o"
for v in my_dictionary.values():
  print(v)

# + [markdown] id="n4ohtpYDKRPY"
# If you want both the keys and the values without needing to lookup up `my_dictionary[k]`, you can ask the dictionary for its `items()`.

# + id="bGGDRbzPKIf9"
for (k, v) in my_dictionary.items():
  print(f"{k}: {v}")

# + [markdown] id="ivbCdgpXKn0z"
# You can also use `for` to operate on a string character by character. Each item in a string is a single character.

# + id="gt5Ru7HjKkjp"
for c in "this string":
  print(c)

# + [markdown] id="H2y1EkjmK6eL"
# If you want to iterate over a list or tuple and need the index of each item, you can use the `range` function along with the `len` function to get the indices of the list or tuple.

# + id="rGqDXpv6KwYy"
for i in range(len(my_list)):
  print(f"{i}: {my_list[i]}")

# + [markdown] id="7vazK2DeLIxU"
# `range` is a function that returns a sequence of numbers. It can take one argument, two arguments, or three arguments.
#
# When you give one argument to `range`, it is considered to be the end of the range (exclusive).

# + id="7n2oLEzGLXw7"
for i in range(5):
    print(i)

# + [markdown] id="JNHvCaZcLbOF"
# When you give two arguments to `range`, they are considered to be the start (inclusive) and end (exclusive) of the sequence.

# + id="C8dmRqSPLqQV"
for i in range(6, 12):
    print(i)

# + [markdown] id="RdmqeIAdLtDK"
# When you give three arguments to `range`, they are considered to be the start (inclusive), end (exclusive), and step size of the sequence.

# + id="tu933OcnL3eq"
for i in range(20, 100, 10):
    print(i)

# + [markdown] id="bxhdbYLYL7tY"
# Ranges are lazily evaluated so even very large ranges will not occupy a significant amount of memory.

# + [markdown] id="rLKvNuQlxkFg"
# `for` loops can also be useful for making a list. For example, if we want to generate a list of random numbers, we could use the `random` library within a `for` loop.

# + id="goP7QXiSiu1v"
import random

random_numbers = []
for i in range(10):
    random_numbers.append(random.randint(0, 10))
print(random_numbers)

# + [markdown] id="iogbsWBcMdbr"
# ## `while` Loops
#
# The `while` loop allows you to repeat a block of code until some arbitrary condition is met.

# + id="Lo1YXGn8M6XT"
counter = 0

while counter < 5:
  print("Not done yet, counter = %d" % counter)
  counter += 1

print("Done, counter = %d" % counter)

# + [markdown] id="HhN1wqr1NAQx"
# `while` loops can be useful in many situations, especially those when you don't know for sure how many times you might need to loop.
#  
# **Note:** You might have also noticed the `+=` operator in the example above. This is a shortcut that Python provides so that we don't have to write out `counter = counter + 1`. There are equivalents for subtraction, multiplication, division, and more.

# + [markdown] id="TwPCggdfN8GP"
# ### `break`
#
# There are times when you might want to exit a loop before it is complete. For this case, you can use the `break` statement.
#
# In the example below, the `break` statement causes the loop to exit after only five iterations, despite having a range of 1,000,000 numbers to iterate.

# + id="CHhiRnYTOCqG"
for x in range(1000000):
  if x >= 5:
    break
  print(x)

# + [markdown] id="MldYIF3jOTF4"
# ### `continue`
#
# `continue` is similar to `break`, but instead of exiting the loop entirely, it just skips the current iteration.
#
# Let's see this in action with a loop that prints numbers between 0 and 7 except 4 and 6.

# + id="awLAZUqCOfCo"
for x in range(10):
  if x == 4 or x == 6:
    continue
  print(x)


# + [markdown] id="u9DuFDG_p2rz"
# ## Functions
#
# Functions are a way to organize and re-use your code. Functions allow you to take a block of your code, give it a name, and then call that code by name as many times as you need to.
#
# Functions are defined in Python using the `def` statement.

# + id="BVpNQ4swQr3u"
def my_function():
  print("I wrote a function")

my_function()
my_function()
my_function()


# + [markdown] id="6NcFKBi2Q8De"
# Standard function definitions always begin with the `def` keyword followed by the name of the function. Function naming follows the same rules as variable naming.
#
# All function definitions are composed of:
#
# - The `def` keyword which tells Python you are about to define a function
# - Any arguments that you want to pass to the function, wrapped in parentheses (more details on this below)
# - A colon to end the statement
#
# The function's code is indented under the function definition.

# + [markdown] id="upFwmsk3R5R4"
# The arguments that come between the parentheses hold the names of variables that can be used in the function. Function arguments, also called parameters, are used to provide the function with data.
#
# **Note:** The reason `my_function` above has nothing within the parentheses is because that particular function has no arguments. A function can have zero or more arguments.

# + id="m0Cm8vbzSOxo"
def doubler(n):
  print(n*2)

doubler(2)


# + [markdown] id="tPhUa3P7T9W3"
# Instead of just printing an output, functions can also return data.

# + id="CB1b7Q-dUJkc"
def doubler(n):
  return n*2

print(doubler(2))


# + [markdown] id="dECmjCN-UU0d"
# Functions can return multiple values as a tuple. The following function returns the minimum and maximum (in that order) of the numbers in a list or tuple.

# + id="umXslWytUXkT"
def min_max(numbers):
  min = 0
  max = 0
  for n in numbers:
    if n > max:
      max = n
    if n < min:
      min = n
  return min, max

print(min_max([-6, 78, -102, 45, 5.98, 3.1243]))


# + [markdown] id="3sFqKkIuVCfv"
# It is important to note that when you pass data to a function, the function gets a *copy* of the data. For numeric, boolean, and string data types, that means that the function can't directly modify the data you passed in. For lists and dictionaries, it is a little more complicated. The function gets a *copy of the location/address* of the data structure. While the function can't change that address, it *can* modify the data structure.
#
# Let's see some examples to solidify the point. In this first example we can see that the number changer can't make any changes to the variable `my_number`.

# + id="FMpdW55IVWCJ"
def number_changer(n):
  n = 42

my_number = 24
number_changer(my_number)
print(my_number)


# + [markdown] id="rGdDthSaWLGt"
# The same is true for booleans. The function below can't modify `my_bool`.

# + id="0IKK0XRaWPxT"
def bool_changer(b):
  b = False

my_bool = True
bool_changer(my_bool)
print(my_bool)


# + [markdown] id="8cyusJcMWbD9"
# The same is true for strings.

# + id="BiIbX1b2WeGm"
def string_changer(s):
  s = "Got you!"

my_string = "You can't get me"
string_changer(my_string)
print(my_string)


# + [markdown] id="2yJ03-RCWrcs"
# However, lists can be modified. See the example below.

# + id="7Wd-GGGeWwBY"
def list_changer(list_parameter):
  list_parameter[0] = "changed!"

my_list = [1, 2, 3]
list_changer(my_list)
print(my_list)


# + [markdown] id="wGqGcQvbXAKJ"
# What do you think the code below will do?

# + id="D-qahKSCXJj6"
def list_changer(list_parameter):
  list_parameter = ["this is my list now"]

my_list = [1, 2, 3]
list_changer(my_list)
print(my_list)


# + [markdown] id="E7qbZizOwl1k"
# Functions cannot change the value of the entire list, they can only change individual values within the list.

# + [markdown] id="mgArUXxCXT_6"
# Dictionaries interact with functions exactly like lists do.

# + id="-hbfpZx5Xb2E"
def dictionary_changer(d):
  d["my_entry"] = 100

my_dictionary = {"a": 100, "b": "bee"}
dictionary_changer(my_dictionary)
print(my_dictionary)


# + id="xcpPFrnZXtwM"
def dictionary_changer(d):
  d = {"this is": "my dictionary"}

my_dictionary = {"a": 100, "b": "bee"}
dictionary_changer(my_dictionary)
print(my_dictionary)


# + [markdown] id="i7dfFTnqX2O2"
# So, how can you get a function to modify a number, bool, or string? You can simply assign the return value of the function to the original variable.

# + id="xM5nDmNuYA2N"
def number_changer(n):
  return n + 1

def boolean_changer(b):
  return not b

def string_changer(s):
  return s.upper()

my_number = 42
my_bool = False
my_string = "Python"

my_number = number_changer(my_number)
my_bool = boolean_changer(my_bool)
my_string = string_changer(my_string)

print(my_number)
print(my_bool)
print(my_string)


# + [markdown] id="w1CW590FbAy4"
# ###  Pass
#
# `pass` is a Python keyword that is used as a placeholder when code hasn't been written yet. You'll see `pass` often in your exercises as a placeholder for the code you'll need to write.

# + id="_NA64T75bXAU"
def do_nothing_function():
  pass

do_nothing_function()


# + [markdown] id="o9rgbpgRNRpy"
# ### Exercise 4

# + [markdown] id="LXjVsAFwKl0s"
# Write a function that implements rock, paper, scissors below. Your function should take in the player's choice of rock, paper, or scissors and plays a game against the computer, which chooses randomly. Feel free to copy code from the "Conditional Decisions" section.

# + [markdown] id="CB_xwusBKC4I"
# #### **Student Solution**

# + id="toyYdLMWRT68"
def rock_paper_scissors(player_choice):
  # Add code here that takes in the players choice of rock, paper, or scissors
  # and plays a game against the computer, which chooses randomly.
  # You can copy code from the "Conditional Decisions" section if you like.
  pass


# + id="Vk_s5QMnTf-Y"
rock_paper_scissors("rock")
rock_paper_scissors("paper")
rock_paper_scissors("scissors")

# + [markdown] id="dnKNAm9bJt4a"
# ---

# + [markdown] id="D_bKEdYtZdFd"
# ## Comments
#
# Comments are simply pieces of your code that will be skipped over when the program is running. While this may sound meaningless, comments are a crucial way for you to communicate with readers of your code (which often includes your future self) about what your code does. Do not underestimate the importance of good code commenting!
#
# Python considers the hash (`#`) to be the start of a comment. This hash can be anywhere in a line. Anything after the hash on the same line won't be executed.
#
# Let's look at an example.

# + id="y6OIz3kWaL5a"
# This is a comment used to document.
# If I need more than one line,
# then I need to add more hashes.

print("Hello") # Comments don't have to be at the start of a line

# print("This won't run")

# + [markdown] id="rgjTJDgFahSC"
# # More Exercises

# + [markdown] id="FwMbxGGgfqU3"
# ## Practice Problems
#
# In case you want to go back and look at the previous exercises, we've made some links here to make it easy to go back and find them.
#
# *   [Printing](#scrollTo=ED0MmXO9Ytkl)
# *   [Integers](#scrollTo=fqQQ2FVTN0__)
# *   [Booleans](#scrollTo=l1kunoRTl5GQ)
# *   [Functions](#scrollTo=o9rgbpgRNRpy)
#
# Once you feel comfortable with the concepts we've covered, you can move on to the challenge problems below.

# + [markdown] id="5CORN6WcalLz"
# ## Exercise 5

# + [markdown] id="KlzvSZ13rW19"
# In the code block below, complete the function by making it return the number cubed.

# + [markdown] id="31lkzGYRxu9P"
# ### Student Solution

# + id="-9DYcdrWa3cJ"
def cube(n):
  pass # your code goes here

print(cube(5))


# + [markdown] id="DSwxHrmpVok3"
# ---

# + [markdown] id="vCd361qEbiRB"
# ## Exercise 6

# + [markdown] id="TuYoUIixrY7T"
# In the code block below, complete the function by making it return the sum of the even numbers of the provided sequence (list or tuple).

# + [markdown] id="XUvoqKWNx5hV"
# ### Student Solution

# + id="TYCBS1xXbsoW"
def sum_of_evens(seq):
  pass # your code goes here

print(sum_of_evens([5, 14, 6, -2, 0, 45, 66]))

# + [markdown] id="jxJbGl3xVtW2"
# ---

# + [markdown] id="SNzbvnMYbwff"
# ## Exercise 7

# + [markdown] id="VPx_YEOkrbHJ"
# We've provided a helper function for you that will take a random step, returning either -1 or 1. It is your job to use this function in another function that takes in a starting value `start` and an ending value `end`, and goes on a random walk from `start` until it reaches `end`. Your function should return the number of steps required to reach `end`. (Note that it may help for debugging to print the value of the random walk at each step.)

# + id="mFVVIgnneQeX"
import random  

def random_step():
  # Returns either -1 or 1 at random 
  return random.choice([-1, 1])


# + [markdown] id="xfOuW44xx_6C"
# ### Student Solution

# + id="p4ZsxD5vcbsL"
def random_walks(start, num_steps, num_trials):
  pass # your code goes here

print(random_walks(0, 5))

# + [markdown] id="csOdifBNWH0o"
# ---
