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¶
# 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
#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
#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
#Python is also a calculator
print(5+10*5)
print((64**(0.5))/4)
55 2.0
Types and slicing¶
#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¶
song_name = "Hello, goodbye"
type(song_name)
str
#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
#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
#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¶
#Those are quite straightforward:
number_int = 5
number_float = 5.0
type(number_int)
int
type(number_float)
float
Lists¶
#A python list is an ordered set of objects. It can contain any other type of variable, including nested lists.
list_firstname = ["Alfred", "Anna", "Clément", "Loan", "Antoine"]
type(list_firstname)
list
#To slice "from the end", you may use negative indexes.
list_firstname[-2]
'Loan'
#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']
#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¶
# A tuple is an other type of ordered set of objects
tuple_name = ("Galichon", "Vlasova", "Montes", "Tricot", "Chapel")
type(tuple_name)
tuple
#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¶
#Dictionary is an unordered set of objects with some useful properties:
dict_fruit = {"apple": 27, "pear": 12}
dict_fruit.items()
dict_items([('apple', 27), ('pear', 12)])
dict_fruit.keys()
dict_keys(['apple', 'pear'])
dict_fruit.values()
dict_values([27, 12])
dict_fruit["apple"]
27
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¶
# One last type that will be useful here is the Boolean. A Boolean can be "True" or "False".
type(True)
bool
#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
print(True and False)
False
print(True or False)
True
# "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.
grade1 = 9.5
grade2 = 12.001
if grade1 >= 10 or grade2 > 12:
print('You passed')
You passed
#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
#"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
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.
#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]
#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.
#You can create functions with the keyword "def"
def return_square(x):
return x**2
x = 5
x_squared = return_square(x)
print(x_squared)
25
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
grade_list_bis = grade_list.copy()
attribute_comment(grade_list_bis)
['Better luck next time !', 'You passed with a good grade', 'You passed with an excellent grade !', 'Better luck next time !', 'You passed.']
Classes¶
# 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
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))
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'>
Nathan_grades.courselist
['Maths', 'Stats']
Nathan_grades.gradelist
[15, 20]
Nathan_grades.commentlist
[]
Nathan_grades.attribute_comments()
Nathan_grades.send_grades()
[('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
import numpy as np
matrix = np.array([[1, 2, 3],
[4, 5, 6]])
print(matrix)
print(type(matrix))
[[1 2 3] [4 5 6]] <class 'numpy.ndarray'>
#Numpy arrays are mutable objects
print(matrix.shape)
(2, 3)
for i in range(matrix.shape[1]):
matrix[:, i] = matrix[:, i]*(10**i)
matrix[0, :]
array([ 1, 20, 300])
#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]]
#Sum:
matrix1 + matrix2
array([[ 7, 9, 12], [14, 16, 18]])
#Elementwise product:
matrix1 * matrix2
#or
np.multiply(matrix1, matrix2)
array([[ 6, 14, 27], [40, 55, 72]])
#Dot product:
matrix1 @ matrix2.T
#or
np.dot(matrix1, matrix2.T)
array([[ 47, 68], [113, 167]])
#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. ]]
# 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
#test your function:
grade_students(grade_array)
Basic Optimisation with Scipy¶
from scipy.optimize import minimize
def f(x):
return (x[0]-1)**2 + (x[1]-1)**2
minimize(f, x0 = [0, 0])
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¶
import matplotlib.pyplot as plt
x = np.linspace(0, 2*np.pi, 1000) #create a vector with 1000 ordered entries going from 0 to 4*pi
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)
plt.plot(x, y_s, c='r', lw=1)
plt.plot(x, y_c, c='g', lw=1)
plt.show()
y_f = x**2
plt.plot(x, y_f)
plt.axis([0, 5, 0, 40])
plt.show()