import numpy as npWeek 2 — Fundamentals of Programming in Python
Programming and Numerical Methods for Economics (ECNM10115) · The University of Edinburgh
Learning goals. After this notebook you can: work with Python’s main data types (integers, floats, booleans, strings, lists, tuples, dictionaries); use NumPy arrays fluently; and control your programs with loops, conditionals, and functions.
Run every cell (
Shift+Enter), attempt each Your turn before opening the solution, and predict outputs before running the Quick check cells.
Indentation
Python uses indentation (4 spaces) to define blocks — where other languages use braces or end. Get it wrong and the code means something different. Here is the log-utility function economists use everywhere:
def log_u(c):
if c < 1e-8:
u = -np.inf # one indent level: inside the if-block
else:
u = np.log(c)
return u
print(log_u(2.0))
print(log_u(-1.0))0.6931471805599453
-inf
Quick check ✋ — these two snippets contain the same lines. Why do they print different things?
for i in [1,2]: for i in [1,2]:
for j in [5,6]: print(i)
print(i,j) for j in [5,6]:
print(j)Answer
Left: the second loop is inside the first (nested) — it prints every (i,j) combination. Right: the loops run one after the other. Only the indentation differs!Integers and floats
The computer distinguishes integers from floats because floats carry more information while integer arithmetic is faster and exact.
y = 2 # integer
x = 2.0 # float
print([type(y), type(x)])
print(1/2) # normal division -> 0.5 (a float)
print(1//2) # integer division -> 0[<class 'int'>, <class 'float'>]
0.5
0
# Converting to float
print(float(y), float('4'), float(True))
# Not-a-number: impossible operations return nan rather than crashing
print(np.log(-1))
# Handy built-ins
a = 2.4
print(round(a), abs(-23))2.0 4.0 1.0
nan
2 23
/tmp/ipykernel_4010/103602420.py:5: RuntimeWarning: invalid value encountered in log
print(np.log(-1))
Augmented assignment
x += 1 is shorthand for x = x + 1. You will use this constantly to accumulate results.
x = 3
x += 1
print(x) # 4
x = 3
x *= 2
print(x) # 6
x = 3
x /= 2
print(x) # 1.54
6
1.5
Booleans
Booleans are a numeric type in Python: True behaves like 1 and False like 0.
x = True
y = 100 < 0 # comparisons produce booleans
print(type(y), y)
print(x + y, x * y) # arithmetic with booleans
bools = [True, True, False, True]
print(sum(bools)) # counts the Trues!
print(bool(3)) # bool(x) is equivalent to x != 0<class 'bool'> False
1 0
3
True
# Compound boolean logic
x, z, y = True, 100 > 10, 100 < 0
print(x and (z or y)) # True
print((x or z) and y) # False
a, b, c = 5, 1, 'you'
print(b > 0 and a < 10 or len(c) > 3)
print(b > 0 and a < 10 and len(c) > 3)True
False
True
False
Strings
str1 = 'This is a string in Python'
str2 = str(4.29) # convert a number to a string
print(str1[0]) # 'T' — strings are indexed like lists
print(str1[10:16]) # slicing: 'string'
# Strings are IMMUTABLE — you cannot change them in place:
try:
str1[9] = 'a'
except TypeError as e:
print('Error:', e)T
string
Error: 'str' object does not support item assignment
# String operators
a = 'black'
b = 'pepper'
print(a + b) # concatenation
print(3 * b) # repetition
print('x' in b) # membership testblackpepper
pepperpepperpepper
False
Lists
Lists are Python’s workhorse container: ordered, sliceable, and mutable — and they can hold anything, even functions.
y = lambda x: x**2 # a function object
list_obj = [1, 2, 3, 'a', 'b', y] # numbers, strings, and a function in one list
list_obj2 = [[1, 2, 3], ['a', 'b']] # a list of lists
print(list_obj[0]) # indexing
print(list_obj[1:3]) # slicing
print(list_obj2[0][0]) # first element of the first inner list1
[2, 3]
1
list_1 = [1, 2, 3]
list_1.append(4) # add one element at the end
print(list_1)
list_1.remove(2) # remove an element
print(list_1)
list_1.append([4, 5, 6]) # append adds the WHOLE list as one element
print(list_1)
list_1.remove([4, 5, 6])
list_1.extend([4, 5, 6]) # extend adds each element separately
print(list_1)
list_1.reverse()
print(list_1)
print(list_1.index(5)) # where is 5?[1, 2, 3, 4]
[1, 3, 4]
[1, 3, 4, [4, 5, 6]]
[1, 3, 4, 4, 5, 6]
[6, 5, 4, 4, 3, 1]
1
Your turn 2.1 — Start from prices = [101.2, 99.8, 103.4]. Append 105.1, then compute the list of returns \(r_t = p_t/p_{t-1} - 1\) using a loop, and print it.
Predict first: how many returns should a list of 4 prices produce?
prices = [101.2, 99.8, 103.4]
# Your attempt here:💡 Click to reveal the solution — but try it yourself first!
prices.append(105.1)
returns = []
for t in range(1, len(prices)):
r = prices[t] / prices[t-1] - 1
returns.append(r)
print(returns)NumPy arrays
Arrays are how we do serious numerical work. Unlike lists, they support fast element-wise maths.
flat_a = np.array([1, 2, 3, 4, 5, 6]) # 1-D (flat) array
print(flat_a[0], flat_a[3:])
a = np.array([[1, 2, 3], [4, 5, 6]]) # 2-D array (a matrix)
print(a)
print(a.shape) # (rows, columns)
print(a[0, :]) # first row, all columns
print(a[:, 1]) # second column1 [4 5 6]
[[1 2 3]
[4 5 6]]
(2, 3)
[1 2 3]
[2 5]
# Standard arrays you will create all the time
zeros = np.zeros(10)
print(zeros.reshape((2, 5))) # reshape returns a NEW array...
zeros = zeros.reshape((2, 5)) # ...assign it to keep the change
m1 = np.ones((3, 4)) # 3x4 matrix of ones
b = np.eye(4) # identity matrix
x = np.linspace(0, 99, 100) # 100 evenly spaced points from 0 to 99
print(np.linspace(2, 4, 5))[[0. 0. 0. 0. 0.]
[0. 0. 0. 0. 0.]]
[2. 2.5 3. 3.5 4. ]
# Array methods
print(x.sum(), x.min(), x.mean(), x.max())
print(x.argmax()) # INDEX of the maximum element
a = np.array([[1, 2, 3], [4, 5, 6]])
# Operations along axes: axis=0 works column-wise, axis=1 row-wise
print(a.sum()) # all elements
print(a.sum(axis=0)) # per column
print(a.sum(axis=1)) # per row
print(a.mean(axis=0))
# NumPy functions apply element-wise
print(np.log(a))4950.0 0.0 49.5 99.0
99
21
[5 7 9]
[ 6 15]
[2.5 3.5 4.5]
[[0. 0.69314718 1.09861229]
[1.38629436 1.60943791 1.79175947]]
# Comparisons are element-wise too
print(a > 3)
print((a > 3).any(), (a > 3).all())
# argmax on a matrix returns the position in the FLATTENED array —
# use unravel_index to convert it to (row, column):
idx = a.argmax()
i_row, i_col = np.unravel_index(idx, a.shape)
print(a[i_row, i_col]) # the maximum element
print(a.T) # transpose[[False False False]
[ True True True]]
True False
6
[[1 4]
[2 5]
[3 6]]
Your turn 2.2 — Create the matrix M = np.array([[3., 7., 1.], [9., 2., 5.]]). Find: (a) the column-wise means, (b) the position (row, column) of the largest element, (c) how many elements are greater than 4.
# Your attempt here:💡 Click to reveal the solution — but try it yourself first!
M = np.array([[3., 7., 1.], [9., 2., 5.]])
print(M.mean(axis=0)) # (a)
i, j = np.unravel_index(M.argmax(), M.shape)
print((i, j)) # (b) -> (1, 0)
print((M > 4).sum()) # (c) booleans sum as 1/0 -> 3Tuples
Tuples look like lists but are immutable — perfect for things that shouldn’t change, and for functions that return several values.
y = ('a', 'b')
z = 4, 5 # parentheses are optional
print(y[0])
try:
y[0] = 10 # tuples are immutable
except TypeError as e:
print('Error:', e)
z1, z2 = z # unpacking (as with unravel_index above)
print(z1, z2)a
Error: 'tuple' object does not support item assignment
4 5
Dictionaries
Dictionaries map keys to values — ideal for keeping model parameters tidy.
individual = {'name': 'Francis', 'age': 28, 'weight': 100}
print(individual['age'])
print(individual['weight'])
# A very common economics pattern: a parameter dictionary
params = {'alpha': 0.3, 'beta': 0.96, 'delta': 0.05}
print(params['beta'])28
100
0.96
Loops
# Accumulate results by appending to a list
squares = []
for i in range(10):
squares.append(i**2)
print(squares)[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
# Loop directly over items — and use enumerate when you also need the index
list_verbs = ['run', 'sit', 'jump', 'play', 'dance']
for i, verb in enumerate(list_verbs):
print('Verb ' + str(i+1) + ': ' + verb) # indexing starts at 0, so +1
# zip loops over two lists in tandem
list_verbs_spanish = ['correr', 'sentarse', 'saltar', 'jugar', 'bailar']
for v_eng, v_spa in zip(list_verbs, list_verbs_spanish):
print(v_eng + ': ' + v_spa)Verb 1: run
Verb 2: sit
Verb 3: jump
Verb 4: play
Verb 5: dance
run: correr
sit: sentarse
jump: saltar
play: jugar
dance: bailar
# Nested loops: the inner loop runs fully for each step of the outer loop
for i in range(6, 8):
print('Multiplication table of', i)
for j in range(1, 11):
print(i, "*", j, "=", i*j)Multiplication table of 6
6 * 1 = 6
6 * 2 = 12
6 * 3 = 18
6 * 4 = 24
6 * 5 = 30
6 * 6 = 36
6 * 7 = 42
6 * 8 = 48
6 * 9 = 54
6 * 10 = 60
Multiplication table of 7
7 * 1 = 7
7 * 2 = 14
7 * 3 = 21
7 * 4 = 28
7 * 5 = 35
7 * 6 = 42
7 * 7 = 49
7 * 8 = 56
7 * 9 = 63
7 * 10 = 70
# While loops: repeat until a condition fails
count = 1
while count < 5:
print(count)
count += 11
2
3
4
# While loops shine when you don't know in advance how many steps you need —
# here: searching for the solution of 3 = x/2, starting from a guess
LHS = 3
eps = 0.001
x = 20
RHS = x / 2
count = 1
while np.abs(LHS - RHS) > eps:
x -= 1
RHS = x / 2
count += 1
if np.abs(LHS - RHS) < eps:
print('solution is x =', x)
elif count == 20:
print('no solution found with', count, 'iterations')
break
else:
continuesolution is x = 6
x = 20 to x = 100 above and re-run. Why does it fail to find the solution?
Answer
Starting from 100 and stepping down by 1, after 20 iterations x is only at ~80 — far from the solution x=6 — and thecount==20 guard stops the search. Iterative algorithms need either enough iterations or a smarter update rule. This idea — convergence and iteration limits — returns in every numerical method later in the course.
Conditional statements
import random
x = random.random()
print('Random number is x =', x)
if x > 0.3 and x < 0.5:
print("You win a prize of $1, congratulations!")
else:
print("Sorry, you win nothing!")Random number is x = 0.1890908435474722
Sorry, you win nothing!
# Combining while + if/else: classify numbers as even or odd
n = 7
while n > 0:
if n % 2 == 0:
print(n, 'is an even number')
else:
print(n, 'is an odd number')
n = n - 17 is an odd number
6 is an even number
5 is an odd number
4 is an even number
3 is an odd number
2 is an even number
1 is an odd number
Functions
def say_hi(name):
return 'Hi ' + name + '! We welcome you at the econ-programming course in the UoE.'
print(say_hi('Brad Pitt'))Hi Brad Pitt! We welcome you at the econ-programming course in the UoE.
# Positional vs keyword arguments — keyword arguments have default values
def f(x, y, a=2, b=2):
return x**a + y*b
print(f(2, 4)) # use both defaults
print(f(2, 4, b=6)) # override one
print(f(2, 4, a=6, b=3)) # override both12
28
76
Your turn 2.3 — Write a function ces(K, L, alpha=0.5, sigma=2.0, A=1.0) implementing the CES production function
\[Y = A\left(\alpha K^{\frac{\sigma-1}{\sigma}} + (1-\alpha)L^{\frac{\sigma-1}{\sigma}}\right)^{\frac{\sigma}{\sigma-1}}\]
and evaluate it at K=4, L=9. (This function stars in PS1 and this week’s lab.)
# Your attempt here:💡 Click to reveal the solution — but try it yourself first!
def ces(K, L, alpha=0.5, sigma=2.0, A=1.0):
rho = (sigma - 1) / sigma
return A * (alpha * K**rho + (1 - alpha) * L**rho)**(1 / rho)
print(ces(4, 9))6.25.
Self-check quiz
Q1. What is1//2?
Answer
0 — integer division truncates.sum([True, False, True, True]) = ?
Answer
3 — booleans are numeric.Answer
Only the list. Tuples and strings are immutable.a, what does a.sum(axis=0) compute?
Answer
Column sums (axis 0 runs down the rows).Before the lab
- Finish Your turn 2.1–2.3 — the CES function is directly relevant to PS1.
- The lab gives tips for PS1 Exercises 1–2; review the Python refresher sections it points to.
- Reference: QuantEcon — Python Essentials.
Next week: data manipulation and data analysis with pandas.