Lecture 0: Introduction to Python
¶

Antoine Chapel (Sciences Po & PSE)
¶

Alfred Galichon's math+econ+code prerequisite class on numerical optimization and econometrics, in Python
¶

Class content by Antoine Chapel. Past and present support from Alfred Galichon's ERC grant CoG-866274 is acknowledged, as well as inputs from contributors listed here. If you reuse material from this class, please cite as:

Antoine Chapel, 'math+econ+code' prerequisite class on numerical optimization and econometrics, January 2023

This is a very short introduction to Python programming. You will find a survival kit of how to read and write your own Python code. There are many excellent tutorials online, from the official one to the set of tutorials proposed on QuantEcon (Sargent & Stachurski). Both of those are more advanced and complete than what you will find here. Python is also a mature language with a lot of online help available. If you encounter an issue or an error message and type it in google, it is likely that someone asked that very same question in 2013 on StackOverflow.

The Basics¶

In [1]:
# This is a comment. It will be ignored by the Python interpreter
# To display something in Python: print

print("Welcome to M+E+C")
Welcome to M+E+C
In [2]:
#variable declaration is easy in Python, you do not need to write the variable type explicitly:

name = "Dominique"
age = 26


#Easy way:
print(name, "is", age, "years old")

#More modern way:
print(f'In {5} years, {name} will be {age+5} years old')
Dominique is 26 years old
In 5 years, Dominique will be 31 years old
In [3]:
#As you see, Python is quite forgiving with type mismatch. Be careful however:
number_string = "12" #this is a string
number_int = 12 #this is an integer


print(2*number_string, 2*number_int)
#The result is clearly not the same
1212 24
In [4]:
#Python is also a calculator

print(5+10*5)
print((64**(0.5))/4)
55
2.0

Types and slicing¶

In [5]:
#Python uses 0-indexing: in a list of objects, the first object has index 0. This will be clearer later.
#Let us review the most common variable types that can be used in Python and their mutability:

String variables¶

In [6]:
song_name = "Hello, goodbye"
type(song_name)
Out[6]:
str
In [ ]:
#Slicing: note the 0-indexing of Python. To get the first element, you use the index 0.
print(song_name[0])

#Trying to change a letter in a string will return an error:
song_name[0] = "Y"

#Errors are quite explicit in Python
In [7]:
#You may however select a part of a string and use it in the construction of a new variable:
new_song_name = song_name[0:4] + " Bells"
print(new_song_name)
Hell Bells
In [ ]:
#Exercise 1: try to print "Hello darkness my old friend" using song_name:
beautiful_song_name = song_name[] + ""





print(beautiful_song_name)

Integers and Floats¶

In [8]:
#Those are quite straightforward:
number_int = 5
number_float = 5.0

type(number_int)
Out[8]:
int
In [9]:
type(number_float)
Out[9]:
float

Lists¶

In [10]:
#A python list is an ordered set of objects. It can contain any other type of variable, including nested lists.
In [11]:
list_firstname = ["Alfred", "Anna", "Clément", "Loan", "Antoine"]
type(list_firstname)
Out[11]:
list
In [12]:
#To slice "from the end", you may use negative indexes.

list_firstname[-2]
Out[12]:
'Loan'
In [13]:
#Lists are mutable objects: you can change an element from the list:
list_firstname[-1] = "Jean-Pierre"
print(list_firstname)
['Alfred', 'Anna', 'Clément', 'Loan', 'Jean-Pierre']
In [14]:
#You may add an element to a list by putting it at the very end, using the append method:
list_firstname.append("Jean-Michel")
print(list_firstname)


#Or insert it at some precise index:
list_firstname.insert(2, "Louise-Marie")
print(list_firstname)
['Alfred', 'Anna', 'Clément', 'Loan', 'Jean-Pierre', 'Jean-Michel']
['Alfred', 'Anna', 'Louise-Marie', 'Clément', 'Loan', 'Jean-Pierre', 'Jean-Michel']

Tuples¶

In [15]:
# A tuple is an other type of ordered set of objects

tuple_name = ("Galichon", "Vlasova", "Montes", "Tricot", "Chapel")
type(tuple_name)
Out[15]:
tuple
In [16]:
#Tuples differ from lists because they are immutable:

try:
    tuple_name[-1] = "Pierre"
except:
    print("Error: Tuples are not mutable")
    
#The syntax shown above is a form of basic error management
Error: Tuples are not mutable

Dictionaries¶

In [17]:
#Dictionary is an unordered set of objects with some useful properties:

dict_fruit = {"apple": 27, "pear": 12}
In [18]:
dict_fruit.items()
Out[18]:
dict_items([('apple', 27), ('pear', 12)])
In [19]:
dict_fruit.keys()
Out[19]:
dict_keys(['apple', 'pear'])
In [20]:
dict_fruit.values()
Out[20]:
dict_values([27, 12])
In [21]:
dict_fruit["apple"]
Out[21]:
27
In [22]:
new_harvest  = {"apple": 12, "pear": 1}

#Let's update the values in our dictionary:
dict_fruit["apple"] = dict_fruit["apple"] + new_harvest["apple"]


#Exercise 2: Do the same for pears, but use the following syntax instead:
#To update a variable, instead of x = x + 1, you may use x += 1

dict_fruit["pear"]


print(dict_fruit)
{'apple': 39, 'pear': 12}

Loopings: if, for, while¶

In [23]:
# One last type that will be useful here is the Boolean. A Boolean can be "True" or "False".

type(True)
Out[23]:
bool
In [24]:
#Some boolean operations:
print(f'Is 4 equal to 5 ? {4 == 5}')

print(f'Is 4 lower or equal to 5 ? {4 <= 5}')

print(f'Is 4 strictly higher than 5 ? {4 > 5}')

print(f'Is 4 different from 5 ? {4 != 5}')
Is 4 equal to 5 ? False
Is 4 lower or equal to 5 ? True
Is 4 strictly higher than 5 ? False
Is 4 different from 5 ? True
In [25]:
print(True and False)
False
In [26]:
print(True or False)
True
In [27]:
# "If" is a keyword by which, if the condition stated afterwards is true, the indented code will execute. For example:

grade = 9.5

if grade >= 10:
    print('You passed !')
    
#Nothing happens because the condition x >= 10 is not met.
In [28]:
grade1 = 9.5
grade2 = 12.001

if grade1 >= 10 or grade2 > 12:
    print('You passed')
You passed
In [29]:
#More complex conditions:
average = 15

if average < 10:
    print("Better luck next time !")
elif average >= 10 and average < 14:
    print("You passed.")
elif average >= 14 and average < 18:
    print("You passed with a good grade")
else:
    print("You passed with an excellent grade !")

    
#As you can see, elif checks alternative conditions, and else acts as a catch-all if none of the previous conditions were met.
You passed with a good grade
In [30]:
#"for" loops: they avoid you from having to repeat five times the same operation.
#Do not overuse loops. loops nested inside other loops can get very slow in Python.

listname = ["Joséphine", "Luc", "Kevin", "Leandre"]

for name in listname:
    print(name)
Joséphine
Luc
Kevin
Leandre
In [31]:
grade_list = [8, 15, 19, 0, 12]

for grade in grade_list:
    if grade < 10:
        print("Better luck next time !")
    elif grade >= 10 and grade < 14:
        print("You passed.")
    elif grade >= 14 and grade < 18:
        print("You passed with a good grade")
    else:
        print("You passed with an excellent grade !")
Better luck next time !
You passed with a good grade
You passed with an excellent grade !
Better luck next time !
You passed.
In [32]:
#A useful tool with "for": range
index_list = []

I = 10
for i in range(I):
    index_list.append(i)

print(index_list)
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
In [33]:
#while loops:
index_list_bis = []

i_max = 10
i = 0

while i < i_max: #while: = "as long as" condition
    index_list_bis.append(i)
    i += 1 # increments i by 1: same thing as i = i + 1

print(index_list_bis)
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

Functions¶

You can do a lot in Python using nothing but functions. A function is an input-output machine. It goes beyond the function you use in maths, although you can obviously build your favorite univariate/multivariate function in Python.

In [34]:
#You can create functions with the keyword "def"
def return_square(x):
    return x**2
In [35]:
x = 5
x_squared = return_square(x)
print(x_squared)
25
In [36]:
def attribute_comment(grade_list):
    comment_list = []
    
    for grade in grade_list:
        if grade < 10:
            comment_list.append("Better luck next time !")
        elif grade >= 10 and grade < 14:
            comment_list.append("You passed.")
        elif grade >= 14 and grade < 18:
            comment_list.append("You passed with a good grade")
        else:
            comment_list.append("You passed with an excellent grade !")
            
    return comment_list
In [37]:
grade_list_bis = grade_list.copy()
In [38]:
attribute_comment(grade_list_bis)
Out[38]:
['Better luck next time !',
 'You passed with a good grade',
 'You passed with an excellent grade !',
 'Better luck next time !',
 'You passed.']

Classes¶

In [39]:
# Classes are an object-oriented-programming tool. You can see it as a way to design your own type
#Let us create a simple one, where a teacher wants to automate his comment attribution
In [40]:
class GradeBook: #Object gradebook
    
    def __init__(self, courselist, gradelist): # we attach some variables to this class of objects
        self.courselist = courselist
        self.gradelist = gradelist
        self.commentlist = []
    
    def attribute_comments(self):
        for grade in self.gradelist:
            if grade < 10:
                self.commentlist.append("Better luck next time !")
            elif grade >= 10 and grade < 14:
                self.commentlist.append("You passed.")
            elif grade >= 14 and grade < 18:
                self.commentlist.append("You passed with a good grade")
            else:
                self.commentlist.append("You passed with an excellent grade !")
    def send_grades(self):
        return list(zip(self.courselist, self.gradelist, self.commentlist))
In [67]:
Nathan_grades = GradeBook(["Maths", "Stats"], [15, 20]) #Here we have created an object, Nathan_grades. It is
print(type(Nathan_grades))                              #an instance of the class GradeBook, that contains data.
<class '__main__.GradeBook'>
In [42]:
Nathan_grades.courselist
Out[42]:
['Maths', 'Stats']
In [43]:
Nathan_grades.gradelist
Out[43]:
[15, 20]
In [44]:
Nathan_grades.commentlist
Out[44]:
[]
In [45]:
Nathan_grades.attribute_comments()
In [46]:
Nathan_grades.send_grades()
Out[46]:
[('Maths', 15, 'You passed with a good grade'),
 ('Stats', 20, 'You passed with an excellent grade !')]

NumPy¶

Python has no built-in tool for handling matrices. To do so and much more, we will rely On an external library: Numpy

In [47]:
import numpy as np
In [48]:
matrix = np.array([[1, 2, 3],
                   [4, 5, 6]])
print(matrix)
print(type(matrix))
[[1 2 3]
 [4 5 6]]
<class 'numpy.ndarray'>
In [49]:
#Numpy arrays are mutable objects
In [50]:
print(matrix.shape)
(2, 3)
In [51]:
for i in range(matrix.shape[1]):
    matrix[:, i] = matrix[:, i]*(10**i)
In [52]:
matrix[0, :]
Out[52]:
array([  1,  20, 300])
In [53]:
#Matrix operations:

matrix1 = np.array([[1, 2, 3],
                    [4, 5, 6]])
matrix2 = np.array([[6, 7, 9],
                    [10, 11, 12]])
print(matrix1)
print(matrix2)
[[1 2 3]
 [4 5 6]]
[[ 6  7  9]
 [10 11 12]]
In [54]:
#Sum:
matrix1 + matrix2
Out[54]:
array([[ 7,  9, 12],
       [14, 16, 18]])
In [55]:
#Elementwise product:
matrix1 * matrix2
#or
np.multiply(matrix1, matrix2)
Out[55]:
array([[ 6, 14, 27],
       [40, 55, 72]])
In [56]:
#Dot product:
matrix1 @ matrix2.T
#or
np.dot(matrix1, matrix2.T)
Out[56]:
array([[ 47,  68],
       [113, 167]])
In [57]:
#Exercise 4:
#You have 10 students, who take 5 courses each.

grade_array = np.round(np.random.uniform(8, 20, size = (5, 10)), 2)
print(grade_array)
[[16.3  16.98 19.74 19.49  8.38 12.65 12.17 15.4  17.34 14.94]
 [19.97 11.75 14.29 15.41 11.02 15.39  9.82 19.22  8.64  8.13]
 [14.23  8.27 12.6  11.43  8.81  9.54 13.06 18.01  8.64 15.26]
 [13.07 13.95 13.32 16.54 18.78 12.53 15.34 16.1  18.54 11.87]
 [10.92 14.64 11.61 17.87 18.7  18.41  9.98 14.99 16.61 19.  ]]
In [ ]:
# Fill in the code below so that it returns a numpy array containing the average
# of every student, and a list of comments

def grade_students(grade_array):
    comment_list = []
    avg_list = []
    
    for student_index in range(___.shape[1]):
        student_grades = grade_array[:, ___]
        student_avg = np.___(student_grades) #Look for the numpy function that takes the mean over a vector
        ___.append(student_avg)
        
    for ___ in avg_list:
        if ___ < 10:
            comment_list.___("Insufficient")
        elif ___ >= 10 and ___ < 12:
            comment_list.___("Pass")
        elif ___ >= 12 and ___ < 16:
            comment_list.___("Good")
        else:
            comment_list.___("Excellent")
            
    return np.round(___, 2), comment_list #np.round(, n) rounds up the values to the nth decimal
In [ ]:
#test your function:

grade_students(grade_array)

Basic Optimisation with Scipy¶

In [58]:
from scipy.optimize import minimize
In [59]:
def f(x):
    return (x[0]-1)**2 + (x[1]-1)**2
In [60]:
minimize(f, x0 = [0, 0])
Out[60]:
      fun: 1.0174381484248428e-16
 hess_inv: array([[ 0.75, -0.25],
       [-0.25,  0.75]])
      jac: array([6.36252046e-10, 6.36252046e-10])
  message: 'Optimization terminated successfully.'
     nfev: 12
      nit: 2
     njev: 3
   status: 0
  success: True
        x: array([0.99999999, 0.99999999])

Graphs with Matplotlib¶

In [61]:
import matplotlib.pyplot as plt
In [62]:
x = np.linspace(0, 2*np.pi, 1000) #create a vector with 1000 ordered entries going from 0 to 4*pi
In [63]:
y_s = np.sin(x) #broadcasting: python/numpy understands that we apply the sin function to the whole $x$ vector, and returns 
                #another vector of the same size
y_c = np.cos(x)
In [64]:
plt.plot(x, y_s, c='r', lw=1)
plt.plot(x, y_c, c='g', lw=1)
plt.show()
No description has been provided for this image
In [65]:
y_f = x**2
plt.plot(x, y_f)
plt.axis([0, 5, 0, 40])
plt.show()
No description has been provided for this image