Level 4 · Economics: Strategy and Growth
Until now every decision on this path faced numbers that do not answer back: a price, a budget line, an interest rate, a demand curve. The micro half of this level is about decisions that do: what is best for you depends on what someone else does, and they are working out the same thing about you. That is game theory. You will read payoff tables, find best responses and the equilibria where nobody wants to change alone, see why such an equilibrium can leave everyone worse off, cross out strategies nobody sensible plays, and randomise when no pure choice is stable. Then firms play these games: competing in quantities (Cournot), in prices (Bertrand), with one moving first (Stackelberg), in game trees solved from the end, and in a dilemma repeated season after season.
The macro half asks why some economies are richer than others, and why growth slows down. The Solow model answers with one update rule: capital per worker grows by what is saved and invested, and shrinks by what wears out and by the newcomers who arrive with no machines of their own. From that rule come a steady state, a saving rate that maximises consumption, the catching up of poor economies (and its limits), growth accounting, technical progress, a model whose growth never fades, and a formula for how fast the gap closes.
The other paths meet this one here in specific places. The Solow rule k_{t+1} = f(k_t) is a nonlinear first-order system of the Signals & Control path; near its rest point a gap shrinks by a constant factor each year, like Level 2's output catching up, and replacing the curve by its tangent line there is the linearisation control engineers use. Solving a game tree from its leaves upwards is the recursive game search of the Algorithms path, and checking every possible deviation is exhaustive search. A best response, the most profitable output, and the best saving rate are optimisation problems, the kind the Machine Learning path solves by setting a derivative to zero or searching. Growth accounting turns three data series into a few honest numbers, which is Statistics path work.
Payoff tables and best responses
In a game each player's payoff depends on everyone's choices. Two players who choose at the same time, without seeing each other's choice, are described by a payoff table: one row for each strategy of the row player, one column for each strategy of the column player, and in every cell both payoffs,
payoff[i][j] = (row player's payoff, column player's payoff) when the row player plays i and the column player plays j.
Two bookshops plan a Saturday event. Shop A (rows) can invite a poet, a crime writer or a children's author; shop B (columns) a crime writer or a children's author. The cells give visitors, in tens. A best response to a strategy of the other player is a strategy that does best against it. A changing her choice moves the play up and down a column, so A's best responses to column j come from comparing A's numbers down that column; B's best responses to row i compare B's numbers across that row.
payoff = [[(5, 2), (1, 4)],
[(3, 3), (3, 1)],
[(5, 0), (2, 2)]]
rows, cols = len(payoff), len(payoff[0])
j = 0 # suppose B invites the crime writer
mine = [payoff[i][j][0] for i in range(rows)] # A's payoffs down column 0
top = max(mine)
print("A down column 0:", mine, "-> best responses", [i for i in range(rows) if mine[i] == top])
i = 1 # suppose A invites the crime writer
theirs = [payoff[i][k][1] for k in range(cols)] # B's payoffs across row 1
print("B across row 1:", theirs, "-> best response", [k for k in range(cols) if theirs[k] == max(theirs)])
print("index() keeps only the first of a tie:", mine.index(top))
A down column 0: [5, 3, 5] -> best responses [0, 2]
B across row 1: [3, 1] -> best response [0]
index() keeps only the first of a tie: 0
Against column 0, rows 0 and 2 both give A the top payoff of 5: both are best responses. index, or max with a key, returns only one of them and silently drops the other. Find the top value first, then collect every strategy that reaches it; a comprehension over range keeps them in increasing order.
Common mistakes: comparing A's payoffs across a row (that is B's choice moving, not A's). Using the other player's number in the cell ([1] where [0] is meant). Keeping only the first of a tie. Assuming the table is square.
Practice: Two Kiosks on One Square
Nash equilibrium: nobody wants to switch alone
A pure-strategy Nash equilibrium is a cell in which each player's strategy is a best response to the other's: A cannot do better by changing row while B stays put, and B cannot do better by changing column while A stays put. A clear way to test a cell is to compute what each player could gain by switching alone:
The cell is an equilibrium when neither gain is positive. A tie is not a gain: a player whose strategy ties for the best has no reason to move, so a gain of exactly 0 still counts. Two cafés on a square choose opening hours (early, standard, late):
payoff = [[(4, 3), (0, 1), (2, 3)],
[(1, 0), (3, 2), (2, 2)],
[(2, 1), (1, 4), (3, 0)]]
m, n = len(payoff), len(payoff[0])
for i in range(m):
for j in range(n):
a, b = payoff[i][j]
gain_a = max(payoff[k][j][0] for k in range(m)) - a # down the column
gain_b = max(payoff[i][k][1] for k in range(n)) - b # across the row
line = f"{(i, j)} {payoff[i][j]} A gains {gain_a} B gains {gain_b}"
print(line + (" <- equilibrium" if gain_a <= 0 and gain_b <= 0 else ""))
(0, 0) (4, 3) A gains 0 B gains 0 <- equilibrium
(0, 1) (0, 1) A gains 3 B gains 2
(0, 2) (2, 3) A gains 1 B gains 0
(1, 0) (1, 0) A gains 3 B gains 2
(1, 1) (3, 2) A gains 0 B gains 0 <- equilibrium
(1, 2) (2, 2) A gains 1 B gains 0
(2, 0) (2, 1) A gains 2 B gains 3
(2, 1) (1, 4) A gains 2 B gains 0
(2, 2) (3, 0) A gains 0 B gains 4
Two cells are equilibria, and in both B is exactly indifferent: column 2 would pay B the same. A test written with < 0 (a strict improvement over every alternative) would wrongly reject them. A game can have one equilibrium, several, or none: when one player wants to match the other and the other wants to differ, as in rock, paper, scissors, every cell leaves somebody with a gain. Problems list equilibria in row-major order (by row, then by column), which is the order of a loop over i with a loop over j inside; "none" is an empty list.
Common mistakes: rejecting ties. Checking A against its row instead of its column. Putting the j loop outside the i loop (column-major order). Returning None instead of [] when there is no equilibrium.
Practice: Two Stations, One Evening Slot
Stable but wasteful: the prisoners' dilemma
Two shops on a street decide whether to pay for a billboard (row and column 1) or not (0). Advertising mostly steals customers from the other shop. Whatever the other shop does, advertising pays more (7 > 5 and 2 > 1), so the only equilibrium is that both advertise and earn 2 each, while not advertising would give both 5. Each shop's own best response, used by both, leaves both worse off: a prisoners' dilemma.
Two yardsticks say what "worse" means. The total payoff of a cell is the sum of the two payoffs. And outcome x Pareto-dominates outcome y when x gives both players at least as much and at least one of them strictly more; an outcome nothing dominates is Pareto efficient.
payoff = [[(5, 5), (1, 7)],
[(7, 1), (2, 2)]]
cells = [(i, j) for i in range(2) for j in range(2)]
totals = {cell: sum(payoff[cell[0]][cell[1]]) for cell in cells}
best = max(totals.values())
print("totals", totals, " largest at", [cell for cell in cells if totals[cell] == best])
def dominates(x, y): # does outcome x Pareto-dominate outcome y?
return x[0] >= y[0] and x[1] >= y[1] and (x[0] > y[0] or x[1] > y[1])
print(dominates((5, 5), (2, 2)), dominates((4, 3), (4, 2)), dominates((4, 2), (4, 2)), dominates((7, 1), (5, 5)))
totals {(0, 0): 10, (0, 1): 8, (1, 0): 8, (1, 1): 4} largest at [(0, 0)]
True True False False
The equilibrium (2, 2) is dominated by (5, 5). The second test is the one that trips people up: (4, 3) dominates (4, 2) although the first player gains nothing, because "at least as much for both, strictly more for one" is enough. An outcome never dominates itself or an identical one, and (7, 1) against (5, 5) is a trade-off: neither dominates. Several cells can share the largest total, so a list of them may hold more than one.
Common mistakes: requiring both players to gain strictly. Letting a cell dominate itself. Assuming the cell with the largest total is unique, or that an equilibrium exists.
Practice: The Price War Between Two Bakeries
Dominated strategies, crossed out one by one
Row r is strictly dominated by row s if s gives the row player strictly more than r against every column. A sensible player never plays r, whatever she expects the other to do. (Only single strategies dominate here, not mixtures of them.) If both players know that, each can cross out the other's dominated strategies as well, look at the smaller game, and find more to cross out: iterated elimination. Every comparison uses only the rows and columns that are still left.
Two market stalls each pick one of three pitches:
payoff = [[(3, 1), (2, 4), (4, 0)],
[(1, 2), (1, 3), (5, 1)],
[(2, 0), (0, 2), (3, 3)]]
def row_beats(s, r, cols): # row s strictly better than row r for A, in every remaining column
return all(payoff[s][c][0] > payoff[r][c][0] for c in cols)
def col_beats(d, c, rows): # column d strictly better than column c for B, in every remaining row
return all(payoff[r][d][1] > payoff[r][c][1] for r in rows)
rows, cols = [0, 1, 2], [0, 1, 2]
print("row 0 beats row 2:", row_beats(0, 2, cols), " row 0 beats row 1:", row_beats(0, 1, cols))
rows.remove(2)
print("over rows", rows, "column 1 beats columns 0 and 2:", col_beats(1, 0, rows), col_beats(1, 2, rows))
print(" but not while row 2 was there:", col_beats(1, 2, [0, 1, 2]))
cols.remove(0)
cols.remove(2)
print("over columns", cols, "row 0 beats row 1:", row_beats(0, 1, cols))
rows.remove(1)
print("left:", rows, cols)
row 0 beats row 2: True row 0 beats row 1: False
over rows [0, 1] column 1 beats columns 0 and 2: True True
but not while row 2 was there: False
over columns [1] row 0 beats row 1: True
left: [0] [1]
Row 1 survives the first look (its 5 in column 2 is the best payoff A has), and column 2 is not dominated while row 2 is in play. Each removal exposes the next. With strict dominance the order of removals does not matter: whatever order you cross things out in, the same strategies are left at the end. So it is safe to remove one strategy, look again from the start, and stop only when a whole pass over the remaining rows and columns finds nothing.
Weak dominance (at least as much everywhere, strictly more somewhere) is a different and weaker test:
r, s = [2, 0, 3], [2, 1, 3]
print("strictly:", all(x > y for x, y in zip(s, r)),
" weakly:", all(x >= y for x, y in zip(s, r)) and any(x > y for x, y in zip(s, r)))
strictly: False weakly: True
Row s is never worse than r and better in one column, but ties in two. Removing weakly dominated strategies can depend on the order and can even remove an equilibrium, which is why a problem always says which kind it uses.
Common mistakes: using >= for strict dominance. Comparing over the original columns instead of the remaining ones. Stopping after one pass. Removing items from a list while a for loop runs over that same list.
Practice: Crossing Out the Hopeless Pitches
Mixed strategies: keeping the other side guessing
A tennis server can serve wide (row 0) or at the body (row 1); the receiver leans wide (column 0) or to the body (column 1). The table gives the server's chance of winning the point, in per cent; the receiver wins the rest. Whatever cell you try, someone wants to switch: a predictable server is read, a predictable receiver is aced. So both randomise. A mixed strategy plays each option with a probability: the server serves wide with probability p, the receiver leans wide with probability q. Against the mix q, row i pays the server the expected payoff
The key fact: a player who mixes must be indifferent between the options she mixes, or she would play the better one every time. So the server's indifference does not pin down her own p; it pins down the receiver's q, the one mix that leaves her indifferent. Each player's probability comes from the other player's payoffs. Setting the two rows equal gives one linear equation in q, and the same for p with the receiver's columns:
from fractions import Fraction
server = [[50, 80], # rows: serve wide / at the body; columns: receiver leans wide / to the body
[90, 40]]
receiver = [[100 - v for v in row] for row in server]
# q must make the SERVER indifferent. Row i pays server[i][1] + q * (server[i][0] - server[i][1]).
slope0, slope1 = server[0][0] - server[0][1], server[1][0] - server[1][1]
q = Fraction(server[1][1] - server[0][1], slope0 - slope1)
# p must make the RECEIVER indifferent. Column j pays receiver[1][j] + p * (receiver[0][j] - receiver[1][j]).
p = Fraction(receiver[1][1] - receiver[1][0],
(receiver[0][0] - receiver[1][0]) - (receiver[0][1] - receiver[1][1]))
print("p =", p, " q =", q)
print("server's two rows against q:", *[q * server[i][0] + (1 - q) * server[i][1] for i in range(2)])
print("receiver's two columns against p:", *[p * receiver[0][j] + (1 - p) * receiver[1][j] for j in range(2)])
p = 5/8 q = 1/2
server's two rows against q: 65 65
receiver's two columns against p: 35 35
The server serves wide 5/8 of the time, a number computed entirely from the receiver's payoffs, and wins 65 % of points whichever serve she picks: in a mixed equilibrium a player's expected payoff is the payoff of either of her options against the other's mix. Solving the server's own indifference for p is the classic mistake; it gives a number, just the wrong one.
Fraction keeps the answer exact, reduces it, and moves any minus sign to the numerator. .numerator and .denominator read the two parts:
from fractions import Fraction
x, y = Fraction(-6, -16), Fraction(6, -8)
print(x, y, (x.numerator, x.denominator), (y.numerator, y.denominator), Fraction(0, 5).denominator)
3/8 -3/4 (3, 8) (-3, 4) 1
Common mistakes: finding a player's probability from her own payoffs. Mixing up which probability belongs to row 0 or column 0. Floats such as 0.625 (or worse, 0.16666666666666666) where an exact fraction is asked for. Writing a fraction with a negative denominator.
Practice: Keeping the Inspector Guessing
Cournot: competing in quantities
Two dairies sell milk into one town. Each week each chooses how much to deliver, at the same time, and the milk sells at one price set by the inverse demand curve
Each litre costs a dairy c to produce (a constant marginal cost), so dairy 1 earns (P - c)\,q_1. Its best response to the rival's output R comes from a parabola: with R fixed,
which is 0 at q = 0 and again at q = (a - c - bR)/b, and highest halfway between:
Level 3's monopoly gives the same: given R, the dairy is a monopoly on what is left of demand, P = (a - bR) - bq, whose marginal revenue (a - bR) - 2bq it sets equal to c. (So does Level 3's slope rule: the slope of \pi_1 in q is a - bR - c - 2bq, zero at the top.) The more the rival delivers, the less it pays to deliver yourself. And a dairy cannot deliver a negative amount: when a - c - bR < 0 its best response is 0.
a, b, c = 90, 0.5, 15 # P = 90 - 0.5 Q dollars per thousand litres
def best_response(R):
return max(0.0, (a - c - b * R) / (2 * b))
def profit(q, R):
return (a - b * (q + R) - c) * q
print("best response to 40:", best_response(40), " grid search:", max((profit(q / 10, 40), q / 10) for q in range(1501)))
print("to 0 (a monopoly):", best_response(0), " to 200:", best_response(200))
q1 = q2 = 0.0
for round_ in range(1, 7):
q1, q2 = best_response(q2), best_response(q1) # each answers the other's last output
print("round", round_, ":", q1, q2)
best response to 40: 55.0 grid search: (1512.5, 55.0)
to 0 (a monopoly): 75.0 to 200: 0.0
round 1 : 75.0 75.0
round 2 : 37.5 37.5
round 3 : 56.25 56.25
round 4 : 46.875 46.875
round 5 : 51.5625 51.5625
round 6 : 49.21875 49.21875
In the loop, each dairy keeps answering the other's last delivery, and the pair homes in on 50 and 50, overshooting less each round (the gap is halved and flips sign, a feedback loop of the Signals & Control kind). The resting point is the Cournot equilibrium: a pair of outputs, each a best response to the other. With identical firms both outputs are the same q, and putting R = q into the best response gives one linear equation:
So Q = 100, P = 40 and each dairy earns 25 \cdot 50 = 1250. A single monopoly dairy would deliver 75 at a price of 52.50 and earn 2812.50, more than the two together: each Cournot firm ignores that its extra litre lowers the price on the other firm's milk too. With n identical firms, the others deliver R = (n - 1)\,q, and the same substitution works.
Unequal costs. With different costs the two best-response lines are different and are solved together, by substituting one into the other. And a crossing at a negative output is not an answer:
a, b = 90, 0.5
def br(c, R):
return max(0.0, (a - c - b * R) / (2 * b))
for c1, c2 in ((15, 30), (15, 60)):
q1 = q2 = 0.0
for _ in range(100):
q1, q2 = br(c1, q2), br(c2, q1)
P = a - b * (q1 + q2)
print("costs", c1, c2, "-> outputs", round(q1, 6), round(q2, 6), " price", round(P, 6))
costs 15 30 -> outputs 60.0 30.0 price 45.0
costs 15 60 -> outputs 75.0 0.0 price 52.5
With costs 15 and 30 the lines are q_1 = 75 - 0.5\,q_2 and q_2 = 60 - 0.5\,q_1; substituting gives q_1 = 45 + 0.25\,q_1, so q_1 = 60 and q_2 = 30, as the loop found. With costs 15 and 60, the same algebra gives q_1 = 80 and q_2 = -10: the lines cross where dairy 2 would have to deliver a negative amount. Dairy 2 delivers 0 instead, and then dairy 1 is a monopoly and re-optimises: its best response to 0 is 75, not 80. Check the corner: at 75 thousand litres the price is 52.50, below dairy 2's cost of 60, so 0 really is dairy 2's best response. Clipping the negative output to 0 while keeping 80 is the common error.
Common mistakes: treating the rival's output as if it moved with yours (that is the monopoly's joint choice). Dividing by 2 instead of 2 * b. Clipping one output at 0 and keeping the other firm's two-firm output. Using one firm's cost in the other's best response.
Practice: Bottled Water From Every Spring, Two Mills, One Flour Market
Bertrand: competing in prices
When firms sell an identical product and buyers simply take the cheapest offer, they compete in prices. A school wants 100 yearbooks and two printers bid a price per book in whole cents; the lower bid wins the whole order, and a tie splits it. Printer 0 can print a book for 400 cents, printer 1 for 520. A firm's profit jumps: one cent below the rival it sells everything, at the rival's price it shares, one cent above it sells nothing.
costs = [400, 520]
units = 100
def profit(i, prices):
low = min(prices)
if prices[i] != low:
return 0 # lost the order
k = prices.count(low) # bidders tied at the lowest bid share it
return (prices[i] - costs[i]) * units // k
prices = [519, 520]
print("profits now:", [profit(i, prices) for i in range(2)])
for p in (400, 518, 519, 520, 521):
print("printer 0 bids", p, "->", profit(0, [p, 520]))
print("printer 0's best bid against 520:", max((profit(0, [p, 520]), p) for p in range(1001)))
print("printer 1's best profit against 519:", max(profit(1, [519, p]) for p in range(1001)))
profits now: [11900, 0]
printer 0 bids 400 -> 0
printer 0 bids 518 -> 11800
printer 0 bids 519 -> 11900
printer 0 bids 520 -> 6000
printer 0 bids 521 -> 0
printer 0's best bid against 520: (11900, 519)
printer 1's best profit against 519: 0
Is [519, 520] an equilibrium? Ask each printer, with the other's bid fixed, what its best possible bid earns, including its current bid. Printer 0 already earns the most it can (519 beats sharing at 520 by a lot). Printer 1 can win only by bidding below its own cost, and a tie at 519 loses money too; losing the order earns 0, and that is what it earns now. Neither gains by switching alone, so this is a Bertrand equilibrium: the cheaper printer wins at one cent under its rival's cost. With equal costs, the same test shows that bidding one cent above cost is an equilibrium too: undercutting to cost earns nothing, raising the bid loses the order. Two firms are enough to push the price down to (almost) cost, which is very different from Cournot.
Because the best deviation profit includes the current bid, it is never below the current profit: test best <= current for every firm, never best < current. And let a firm bid high enough to lose on purpose: selling nothing beats selling below cost.
Common mistakes: forgetting that a tie splits the order. Letting a firm's only options be bids that win. Testing best < current, which is never true. Treating prices as continuous when the problem moves in whole cents.
Practice: Undercut by a Cent
Moving first: the Stackelberg leader
Now one firm moves first. A large orchard (the leader) announces its apple harvest for the market; a small orchard (the follower) sees it and then chooses its own. The price is P = 120 - 2Q dollars a crate. A game in which players move one after the other is solved backwards: first work out what the follower will do for every possible q_1, then let the leader, who can predict that, choose the best q_1.
The follower's rule is its Cournot best response, q_2(q_1) = \max\left(0, \frac{a - c_2 - b\,q_1}{2b}\right). With both costs 20 that is q_2 = 25 - q_1/2, and putting it into the price gives P = 120 - 2\,(q_1 + 25 - q_1/2) = 70 - q_1. So the leader's profit is (70 - q_1 - 20)\,q_1 = (50 - q_1)\,q_1, a parabola that peaks at q_1 = 25. The leader knows that every extra crate it commits to makes the follower shrink its own output, and it uses that.
A grid search over the leader's choice, with the follower's rule inside, checks the algebra, and handles the corner where the follower stops producing:
a, b, c1 = 120, 2, 20
def follower(q1, c2):
return max(0.0, (a - c2 - b * q1) / (2 * b))
def leader_profit(q1, c2):
return (a - b * (q1 + follower(q1, c2)) - c1) * q1
for c2 in (20, 60):
best, q1 = max((leader_profit(x / 100, c2), x / 100) for x in range(6001))
q2 = follower(q1, c2)
print("c2 =", c2, ": leader", q1, " follower", q2, " price", a - b * (q1 + q2), " leader earns", round(best, 2))
for q1 in (25, 30, 35):
print("c2 = 60, leader makes", q1, ": follower makes", follower(q1, 60), " leader earns", leader_profit(q1, 60))
c2 = 20 : leader 25.0 follower 12.5 price 45.0 leader earns 625.0
c2 = 60 : leader 30.0 follower 0.0 price 60.0 leader earns 1200.0
c2 = 60, leader makes 25 : follower makes 2.5 leader earns 1125.0
c2 = 60, leader makes 30 : follower makes 0.0 leader earns 1200.0
c2 = 60, leader makes 35 : follower makes 0.0 leader earns 1050.0
With equal costs the leader makes 25 crates, twice the follower's 12.5, and earns 625; in the simultaneous Cournot game each would make 100/6 \approx 16.67 and earn about 555.56. Moving first pays.
With a follower cost of 60, the follower makes nothing once q_1 \ge (120 - 60)/2 = 30, because the price is then at or below its cost. The leader's parabola (while the follower still produces) would peak beyond that point, so the leader chooses exactly 30, the limit output that keeps the follower out: its own monopoly output of 25 would let the follower in (1125), and anything beyond 30 only lowers its own price (1050 at 35). In general: if the leader's best output with the follower producing lies beyond the follower's shut-out point, compare the shut-out point with the leader's monopoly output and take the larger. A grid finds the answer only to its step size; use it to check algebra, not to replace it.
Common mistakes: holding the follower's output fixed while the leader optimises (that is Cournot again). Letting the follower's output go negative. Clipping the follower at 0 but keeping the leader's unconstrained choice. Forgetting that the leader, too, may be best off producing nothing.
Practice: The Old Brewery Moves First
Game trees and threats nobody believes
A sequential game with several moves is a game tree. As Python data, a leaf (the game is over) is a tuple of payoffs, one per player, and a decision node is a dict naming the node, the player who moves there and the list of moves, each a (label, subtree) pair.
A tenant (player 0) is thinking of painting the flat's living room. The landlord (player 1) has said that if she does, he will keep her deposit. Keeping it means going to court, which costs him more than a painted wall:
tree = {"name": "tenant", "player": 0, "moves": [
("paint", {"name": "landlord", "player": 1, "moves": [
("keep deposit", (-2, 0)), ("return deposit", (3, 2))]}),
("leave it", (0, 3))]}
def leaves(node):
if not isinstance(node, dict): # a leaf: a tuple of payoffs
return [node]
found = []
for label, sub in node["moves"]:
found += leaves(sub)
return found
def node_names(node):
if not isinstance(node, dict):
return []
names = [node["name"]]
for label, sub in node["moves"]:
names += node_names(sub)
return names
print(leaves(tree))
print(node_names(tree))
print("the landlord's best hope:", max(p[1] for p in leaves(tree)))
[(-2, 0), (3, 2), (0, 3)]
['tenant', 'landlord']
the landlord's best hope: 3
Each function is recursive: a leaf answers directly, and a decision node combines the answers of its subtrees. isinstance(node, dict) tells the two kinds apart.
The landlord would like the tenant to leave the wall alone (3 is his best hope). But backward induction solves the tree from the end: at the landlord's node, returning the deposit gives him 2 and keeping it 0, so he returns it, and that node is worth (3, 2). At the root the tenant compares painting, now worth 3 to her, with leaving it, worth 0: she paints. The threat is not credible: once the wall is painted, carrying it out would hurt the landlord himself.
A subgame perfect equilibrium is the full plan this produces: a choice at every decision node, including nodes that play never reaches, such that no player would change any choice it is asked to make. A solver has the same shape as leaves: solve every subtree (also the ones the mover will reject, so that their nodes get a choice in the plan), then let the mover keep the subtree whose payoff tuple is best for the mover, payoffs[player], and pass that whole tuple up so the node above can compare. Ties need a rule, and the rule matters to others: if both of the landlord's moves gave him 2, a rule "first move in the list" would make him keep the deposit, and the tenant, foreseeing that, would leave the wall alone.
Python allows about a thousand nested calls by default, which is plenty for trees a few hundred moves deep.
Aside (optional): do people reason backwards? In the lab, games solved by backward induction often go differently. When one player proposes how to split a sum and the other can only accept or reject (rejecting leaves both with nothing), the theory says the smallest positive offer is accepted; in experiments many people reject small offers, and proposers offer much more. Some economists read this as evidence that people care about fairness or reciprocity, so payoffs should include more than money; others point out that behaviour moves towards the prediction in some settings, for example with competition among proposers, and that the model was never meant as a description of how people think. Both lines of work continue. Problems here state the payoffs and the rule, and ask what follows.
Common mistakes: comparing the wrong player's payoff at a node. Solving only the chosen subtree, which leaves other nodes out of the plan. Breaking ties with >=, which picks the last of a tie. Building the path of moves in reverse order.
Practice: Threats That Would Not Be Carried Out
Repeated games: strategies that remember
A prisoners' dilemma played once ends in mutual defection. Played again and again by the same two players, it is a different game: today's move can reward or punish yesterday's. A round pays, with Temptation > Reward > Punishment > Sucker,
| other cooperates (C) | other defects (D) | |
|---|---|---|
| you cooperate (C) | R each |
S to you, T to the other |
| you defect (D) | T to you, S to the other |
P each |
A strategy in the repeated game is a rule from the history of the match so far to this round's move. Three famous ones, in words:
- tit-for-tat cooperates in the first round, then copies whatever the other did in the previous round;
- grim trigger cooperates until the other defects once, then defects for ever;
- win-stay, lose-shift repeats its last move after a good round (
RorT) and switches after a bad one (PorS).
In code a strategy is a function of two lists, its own earlier moves and the other's. Two made-up rules play seven rounds: "majority" cooperates unless the other has defected more often than cooperated so far; "every third" defects in rounds 3, 6, 9, ...
T, R, P, S = 7, 5, 2, 0
table = {("C", "C"): (R, R), ("C", "D"): (S, T), ("D", "C"): (T, S), ("D", "D"): (P, P)}
def majority(mine, theirs):
return "D" if theirs.count("D") > theirs.count("C") else "C"
def every_third(mine, theirs):
return "D" if (len(mine) + 1) % 3 == 0 else "C"
hist1, hist2, score = [], [], [0, 0]
for round_ in range(7):
a = majority(hist1, hist2) # both choose from the history so far ...
b = every_third(hist2, hist1)
gain1, gain2 = table[(a, b)]
score[0] += gain1
score[1] += gain2
hist1.append(a) # ... and only then are the moves recorded
hist2.append(b)
print("".join(hist1))
print("".join(hist2))
print("scores", score)
CCCCCCC
CCDCCDC
scores [25, 39]
"Majority" never sees more defections than cooperations, so it never retaliates and is exploited twice. Two details make simulations right. Both moves of a round are chosen before either is appended; appending a first would let the second player see the current round. And each strategy is called with its own history first: every_third(hist2, hist1). A tournament plays every pair once (for i in range(n): for j in range(i + 1, n):), each match starting from two fresh, empty lists.
Common mistakes: recording one move before the other is chosen. Reusing histories from one match in the next. Passing the histories in the wrong order. Letting a player play itself, or playing each pair twice.
Practice: Seasons on the Shared Bay
Output per worker and diminishing returns
Now the macro half. Level 3 met the Cobb-Douglas production function. For a whole economy with capital K and labour L take Y = A\,K^{\alpha} L^{1-\alpha}, with constant returns to scale. Divide by L and everything can be said per worker: with k = K/L and y = Y/L,
The marginal product of capital (MPK) is the extra output from a little more capital: the slope of y as a function of k, also called its derivative. Level 3's slope rule, q^n has slope n\,q^{n-1}, is the power rule, and it holds for any power p, fractions and negative numbers included: k^p has slope p\,k^{p-1}. As before, a number in front stays in front, c \cdot k has slope c, a constant has slope 0, and a sum is taken term by term. So
The numerical check of Level 3 works for any slope: take a tiny step h and compute (y(k + h) - y(k))/h. Farms on a plain, with k tractors per farmer:
A, alpha = 3, 0.4
y = lambda k: A * k ** alpha
mpk = lambda k: alpha * A * k ** (alpha - 1)
h = 1e-6
for k in (1, 2, 5, 10, 20):
print("k =", k, " y =", round(y(k), 4), " MPK =", round(mpk(k), 4),
" slope from a tiny step:", round((y(k + h) - y(k)) / h, 4))
print("brackets matter:", round(2 ** (alpha - 1), 4), "is not", round(2 ** alpha - 1, 4))
ks = [k / 10 for k in range(1, 301)]
plot(ks, [y(k) for k in ks], xlabel="tractors per farmer", ylabel="tonnes per farmer", title="Output per worker")
k = 1 y = 3.0 MPK = 1.2 slope from a tiny step: 1.2
k = 2 y = 3.9585 MPK = 0.7917 slope from a tiny step: 0.7917
k = 5 y = 5.711 MPK = 0.4569 slope from a tiny step: 0.4569
k = 10 y = 7.5357 MPK = 0.3014 slope from a tiny step: 0.3014
k = 20 y = 9.9434 MPK = 0.1989 slope from a tiny step: 0.1989
brackets matter: 0.6598 is not 0.3195
Output keeps rising, but each extra tractor adds less than the one before: diminishing returns, because the exponent \alpha - 1 is negative. Press Run to see the curve flatten. Since k^{\alpha - 1} = k^{\alpha}/k, the MPK is also \alpha \cdot y / k, \alpha times output per tractor. With \alpha = 1 the MPK would be the constant A; that case returns later in this lesson.
Common mistakes: k ** alpha - 1, which is k^{\alpha} - 1. Forgetting the factor alpha in front of the MPK. Using total capital K where capital per worker k is meant.
Practice: Looms for the Weavers' Cooperative
Capital accumulation, year by year
The Solow model follows capital per worker from year to year. Three things move it:
- a share s of each year's output is saved and invested in new capital: s\,y_t per worker;
- a share \delta of the capital wears out (depreciation): \delta\,k_t;
- the workforce grows by a share n a year, and the newcomers arrive with no capital of their own, so the same machines are spread over more people: capital per worker is diluted by about n\,k_t.
Everything on the right is this year's: this year's output, from this year's capital. The dilution is easy to see with numbers: 100 tractors shared by 50 farmers is 2 each; with 2 % more farmers and not one tractor lost, it is 100/51:
print("100 tractors for 51 farmers:", round(100 / 51, 4), " the model's 2 - n * 2:", 2 - 0.02 * 2)
A, alpha, s, delta, n = 2, 0.4, 0.15, 0.06, 0.02
for k0 in (2, 20, 0):
k, path = k0, [k0]
for year in range(5):
y = A * k ** alpha # this year's output, from this year's capital
k = k + s * y - (delta + n) * k
path.append(k)
print("from", k0, ":", [round(v, 4) for v in path])
100 tractors for 51 farmers: 1.9608 the model's 2 - n * 2: 1.96
from 2 : [2, 2.2359, 2.4709, 2.704, 2.9343, 3.161]
from 20 : [20, 19.3943, 18.825, 18.2895, 17.7858, 17.3116]
from 0 : [0, 0.0, 0.0, 0.0, 0.0, 0.0]
From 2 tractors per farmer capital rises; from 20 it falls, because saving no longer keeps up with wear and newcomers. From 0 nothing is produced, nothing is saved, and capital stays at 0 for ever: a resting point, but a useless one. Writing the rule as k_{t+1} = (1 - \delta - n)\,k_t + s\,y_t shows why problems require \delta + n < 1: then both parts are non-negative, so capital per worker can never turn negative.
Press Run to watch both paths level off at the same height:
A, alpha, s, delta, n = 2, 0.4, 0.15, 0.06, 0.02
for k0 in (2, 20):
k, path = k0, []
for year in range(120):
path.append(k)
k = k + s * A * k ** alpha - (delta + n) * k
plot(path, label=f"from {k0}", xlabel="year", ylabel="tractors per farmer")
Common mistakes: computing output from next year's capital. Leaving out n. Returning T values when the path from year 0 to year T has T + 1. Rounding inside the loop.
Practice: The Fishing Fleet, Year by Year
The steady state: where saving just covers the wear
The paths level off at a steady state: a k^* > 0 that the rule maps to itself, k_{t+1} = k_t = k^* (a fixed point of the rule). There the change is zero:
The left side is investment per worker. The right side is break-even investment: what it takes to replace worn-out capital and equip the newcomers. Below k^* investment is larger and k rises; above it, smaller, and k falls.
Solving needs two rules for powers: k^a / k^b = k^{a-b} and (k^a)^b = k^{ab}. With the numbers of the previous section (sA = 0.3, \delta + n = 0.08, \alpha = 0.4):
The second step divides both sides by k^{0.4} (allowed because k > 0), which leaves k^{1 - 0.4} on the right. The last step raises both sides to the power 1/0.6. The same three steps work with letters.
A, alpha, s, delta, n = 2, 0.4, 0.15, 0.06, 0.02
k_star = (0.3 / 0.08) ** (1 / 0.6)
y_star = A * k_star ** alpha
print("k* =", round(k_star, 4), " y* =", round(y_star, 4), " consumption (1 - s) y* =", round((1 - s) * y_star, 4))
print("investment", round(s * y_star, 4), " break-even", round((delta + n) * k_star, 4))
print("without the brackets:", round(3.75 ** 1 / 0.6, 4))
k = 2.0
for year in range(2000):
k = k + s * A * k ** alpha - (delta + n) * k
print("2000 years from k = 2:", round(k, 4), " within 1e-6 of k*:", abs(k - k_star) <= 1e-6)
k* = 9.0515 y* = 4.8274 consumption (1 - s) y* = 4.1033
investment 0.7241 break-even 0.7241
without the brackets: 6.25
2000 years from k = 2: 9.0515 within 1e-6 of k*: True
At the steady state each worker produces y^* and consumes what is not saved, c^* = (1 - s)\,y^*; saving s\,y^* is investment, not consumption. A long simulation lands on the same number, which is a good check on any algebra. Compare floats with a tolerance, never with ==. Press Run to see the two sides of the equation cross once:
A, alpha, s, delta, n = 2, 0.4, 0.15, 0.06, 0.02
ks = [k / 10 for k in range(201)]
plot(ks, [s * A * k ** alpha for k in ks], label="investment s y", xlabel="capital per worker k")
plot(ks, [(delta + n) * k for k in ks], label="break-even (delta + n) k")
Common mistakes: 1 / 1 - alpha, which Python reads as (1 / 1) - alpha. Raising to 1/\alpha instead of 1/(1 - \alpha). Leaving out n. Reporting s\,y^* as consumption.
Practice: Where Saving Just Covers the Wear
Saving more: a higher level, not faster growth for ever
Should an economy save more? More saving means more capital and more output, but a smaller share of that output is eaten. Steady-state consumption per worker, c^*(s) = (1 - s)\,y^*(s), is a hump: at s = 0 there is no capital, at s = 1 nothing is eaten. Keeping A = 2, \delta + n = 0.08 and \alpha = 0.4, the steady state for saving rate s is k^* = (2s/0.08)^{1/0.6} = (25 s)^{1/0.6}:
alpha = 0.4
for s in (0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.8):
k = (25 * s) ** (1 / 0.6)
y = 2 * k ** alpha
print("s =", s, " k* =", round(k, 2), " y* =", round(y, 3), " c* =", round((1 - s) * y, 3))
k_gold = (25 * 0.4) ** (1 / 0.6)
print("MPK at s = 0.4:", round(alpha * 2 * k_gold ** (alpha - 1), 6))
s = 0.1 k* = 4.61 y* = 3.684 c* = 3.316
s = 0.2 k* = 14.62 y* = 5.848 c* = 4.678
s = 0.3 k* = 28.74 y* = 7.663 c* = 5.364
s = 0.4 k* = 46.42 y* = 9.283 c* = 5.57
s = 0.5 k* = 67.33 y* = 10.772 c* = 5.386
s = 0.6 k* = 91.23 y* = 12.164 c* = 4.866
s = 0.8 k* = 147.36 y* = 14.736 c* = 2.947
MPK at s = 0.4: 0.08
Consumption peaks at s = 0.4, which is \alpha. This is the golden rule saving rate: the one that gives the most steady-state consumption. There the marginal product of capital equals \delta + n: one more unit of capital per worker adds exactly what it costs to maintain, so more saving would cost more consumption than it adds. For a Cobb-Douglas production function the golden rule is always s = \alpha. When several rates are compared by a computed consumption, two that are equal on paper can differ in the last digit, so compare with a tolerance and apply the stated tie rule.
What does a rise in saving do over time? Start at the steady state for s = 0.15 and let the saving rate be 0.25 from year 0's saving onwards:
A, alpha, delta, n = 2, 0.4, 0.06, 0.02
k = (0.3 / 0.08) ** (1 / 0.6) # the old steady state, s = 0.15
y = A * k ** alpha
levels, growth = [y], []
for year in range(1, 301):
k = k + 0.25 * y - (delta + n) * k # the new saving rate, from year 0's saving on
y_next = A * k ** alpha
growth.append(y_next / y - 1)
levels.append(y_next)
y = y_next
for t in (1, 2, 5, 20, 100, 300):
print("year", t, " y =", round(levels[t], 4), " growth", round(growth[t - 1], 5))
print("new steady state y* =", round(A * (0.5 / 0.08) ** (0.4 / 0.6), 4))
year 1 y = 4.9288 growth 0.021
year 2 y = 5.0245 growth 0.01941
year 5 y = 5.281 growth 0.01552
year 20 y = 6.0872 growth 0.00598
year 100 y = 6.7727 growth 0.0001
year 300 y = 6.786 growth 0.0
new steady state y* = 6.786
Growth jumps to 2.1 % and then fades towards zero, while the level of output per worker climbs to its new, higher steady state. In the Solow model, saving more raises the level of income for good, but its growth only for a while: without technical progress, output per worker does not grow at all in the long run. Growth rates on this path are fractions, y_t / y_{t-1} - 1, and a list of yearly rates for years 1, \dots, T has T entries.
Common mistakes: maximising output instead of consumption (output always prefers the highest saving rate). Assuming the rates come sorted. Using the old saving rate for the first step after the change. Starting from the new steady state, where nothing happens.
Practice: How Much Should the Island Save?, After the Thrift Campaign
Convergence: how long, and who grows fastest
Diminishing returns make a poor economy grow fast: each new machine adds a lot of output, and what it saves is large compared with the little capital it has. Near the steady state growth fades. How many years until an economy is "close"? A while loop counts them, and it must test year 0 before the first update, so that an economy already close needs 0 years. Here "close" means output per worker at least 95 % of y^*:
A, alpha, s, delta, n = 2, 0.4, 0.15, 0.06, 0.02
k_star = (0.3 / 0.08) ** (1 / 0.6)
y_star = A * k_star ** alpha
def years_to_95(k0):
k, years = k0, 0
while A * k ** alpha < 0.95 * y_star: # tested in year 0, before any update
k = k + s * A * k ** alpha - (delta + n) * k
years += 1
return years
for k0 in (1, 4, 12):
k1 = k0 + s * A * k0 ** alpha - (delta + n) * k0
first = (A * k1 ** alpha) / (A * k0 ** alpha) - 1
print("k0 =", k0, " years to 95 % of y*:", years_to_95(k0), " output growth in year 1:", round(first, 4))
k0 = 1 years to 95 % of y*: 48 output growth in year 1: 0.0828
k0 = 4 years to 95 % of y*: 34 output growth in year 1: 0.0199
k0 = 12 years to 95 % of y*: 0 output growth in year 1: -0.005
The poorest start grows fastest in its first year, but has the furthest to go. A start above the steady state is already "close" (0 years) and shrinks. The first-year growth rate is of output, y_1 / y_0 - 1, not of capital. Problems often use a two-sided relative band around the steady state, |k_t - k^*| \le \text{frac} \cdot k^*, which scales with the size of the economy; never wait for k == k_star, which a path approaches but may never hit exactly.
Conditional convergence. The Solow model does not say that poor economies always grow faster than rich ones. Each economy heads for its own steady state, set by its own saving, workforce growth and productivity, and it grows fast when it is far below that. One step of the rule shows it: capital per worker grows at s A k^{\alpha - 1} - (\delta + n), investment per unit of capital minus what wears out and is diluted. Three regions with the same \alpha and \delta, ranked by output growth in the coming year:
alpha, delta = 0.4, 0.05
regions = [("Marsh", 1.0, 0.10, 0.03, 2.0), # name, A, s, n, k0
("Hollin", 2.0, 0.25, 0.01, 30.0),
("Ostry", 1.5, 0.20, 0.02, 3.0)]
ranked = []
for name, A, s, n, k0 in regions:
k1 = k0 + s * A * k0 ** alpha - (delta + n) * k0
growth = (k1 / k0) ** alpha - 1 # A cancels in y1 / y0
print(name, " y0 =", round(A * k0 ** alpha, 3), " capital grows at", round(s * A * k0 ** (alpha - 1) - delta - n, 4),
" output at", round(growth, 4))
ranked.append((-growth, name))
print([name for _, name in sorted(ranked)])
print(sorted([(-0.02, "Vell"), (-0.05, "Rusk"), (-0.02, "Aster")]))
Marsh y0 = 1.32 capital grows at -0.014 output at -0.0056
Hollin y0 = 7.796 capital grows at 0.005 output at 0.002
Ostry y0 = 2.328 capital grows at 0.0852 output at 0.0332
['Ostry', 'Hollin', 'Marsh']
[(-0.05, 'Rusk'), (-0.02, 'Aster'), (-0.02, 'Vell')]
Marsh is the poorest region, yet it shrinks: with its low saving and fast-growing workforce, its own steady state is below where it is now. To rank from highest to lowest with ties in alphabetical order, sort pairs (-value, name): the minus sign puts the largest value first, and equal values fall back to comparing names, as the last line shows.
Common mistakes: updating before testing year 0. An absolute band frac where frac * k_star is meant. Reporting capital growth as output growth. Ranking growth by today's income instead of computing each economy's own step. Sorting ascending, or breaking ties by input order.
Practice: Two Valleys, One Destination, Four Harbours, Four Destinations
Growth accounting: what the inputs do not explain
Data show output, capital and labour growing; how much of output growth do the inputs explain? Take growth rates of both sides of Y = A\,K^{\alpha} L^{1-\alpha}. For small rates the growth rate of a product is close to the sum of the growth rates, and a power multiplies a growth rate by its exponent, so
Capital growth counts with weight \alpha, labour growth with 1 - \alpha. A, total factor productivity (TFP), cannot be observed, so it is measured as what is left over, the Solow residual: g_A = g_Y - \alpha\,g_K - (1 - \alpha)\,g_L. Growth rates are year on year, g_X = X_t / X_{t-1} - 1.
Y = [500, 520]
K = [1500, 1560]
L = [200, 202]
alpha = 0.3
g_Y, g_K, g_L = Y[1] / Y[0] - 1, K[1] / K[0] - 1, L[1] / L[0] - 1
capital, labour = alpha * g_K, (1 - alpha) * g_L
print("growth: output", round(g_Y, 4), " capital", round(g_K, 4), " labour", round(g_L, 4))
print("capital explains", round(capital, 4), " labour", round(labour, 4), " residual", round(g_Y - capital - labour, 4))
A = [Y[t] / (K[t] ** alpha * L[t] ** (1 - alpha)) for t in range(2)]
print("A itself grew by", round(A[1] / A[0] - 1, 4))
growth: output 0.04 capital 0.04 labour 0.01
capital explains 0.012 labour 0.007 residual 0.021
A itself grew by 0.0207
Output and capital both grew by 4 %, but capital has a weight of only 0.3, so it explains 1.2 points; labour explains 0.7; the remaining 2.1 points are the residual, more than half of all growth. Computing A directly gives a slightly different 2.07 %, because the equation is an approximation. Problems that use the residual define it by the equation, so compute it exactly as the equation says.
Aside (optional): what growth models leave out. The Solow residual has been called "a measure of our ignorance": everything that changes output without being counted as capital or labour lands in it, from new technology to better management, schooling not counted in labour, how intensively machines are used in a boom, and measurement errors. Economists differ on what drives it most. Some stress technology and research; others stress institutions such as property rights and courts, geography, or how well capital and workers are allocated across firms; and some argue that better measures of input quality shrink the residual considerably. Models like Solow's also leave out the environment and how income is shared. Problems here take each model as stated and ask what follows in it.
Common mistakes: growth over the whole period instead of year on year. Differences X_t - X_{t-1} instead of growth rates. Giving capital the weight 1 - \alpha. Returning an entry for year 0, which has no previous year.
Practice: What the Machines Do Not Explain
Growth that does not fade: better tools and the AK model
In the plain Solow model output per worker stops growing. Two extensions keep it growing.
Technical progress. Let each worker's efficiency E_t rise by a fraction g a year, E_t = E_0\,(1 + g)^t, and let it multiply labour: Y = K^{\alpha} (E L)^{1 - \alpha}, as if each worker counted as E effective workers. Per effective worker, \tilde{k} = K/(EL) and \tilde{y} = \tilde{k}^{\alpha}, and the rule is the Solow rule with one change: effective workers multiply by n (more people) and by g (better tools), so capital per effective worker is diluted by \delta + n + g:
This rule has a steady state, the balanced growth path. Output per worker is y_t = E_t\,\tilde{y}_t: when \tilde{y} is constant, y grows exactly like E, at the rate g.
alpha, s, delta, n, g, E0 = 0.4, 0.2, 0.05, 0.01, 0.02, 1.0
k_star = (0.2 / 0.08) ** (1 / 0.6) # s / (delta + n + g) = 0.2 / 0.08
def per_worker(k0, years):
k, out = k0, []
for t in range(years + 1):
out.append(E0 * (1 + g) ** t * k ** alpha) # y_t = E_t * y~_t
k = k + s * k ** alpha - (delta + n + g) * k
return out
for k0 in (k_star, 1.0):
y = per_worker(k0, 40)
print("k~0 =", round(k0, 4), " growth in years 1, 2 and 40:", [round(y[t] / y[t - 1] - 1, 4) for t in (1, 2, 40)])
k~0 = 4.605 growth in years 1, 2 and 40: [0.02, 0.02, 0.02]
k~0 = 1.0 growth in years 1, 2 and 40: [0.0673, 0.0623, 0.0232]
On the balanced growth path output per worker grows at exactly 2 % a year, the rate of technical progress, whatever the saving rate. Below it, growth is faster at first (2 % plus catching up) and slows towards 2 %. Saving still changes the level, not the long-run growth rate.
The AK model. If "capital" includes knowledge and skills, perhaps it has no diminishing returns at all: y = A\,k, the case \alpha = 1. Then the rule is k_{t+1} = (1 + sA - \delta - n)\,k_t, and capital and output grow at the constant rate sA - \delta - n for ever, faster the more is saved. This is the simplest model of endogenous growth. If the rate is negative, the economy shrinks for ever.
A, s, delta, n = 0.4, 0.25, 0.04, 0.01
k = 5.0
for year in range(4):
k_next = k + s * A * k - (delta + n) * k
print("year", year, "to", year + 1, ": growth", round(k_next / k - 1, 6))
k = k_next
print("s * A - delta - n =", round(s * A - delta - n, 6))
year 0 to 1 : growth 0.05
year 1 to 2 : growth 0.05
year 2 to 3 : growth 0.05
year 3 to 4 : growth 0.05
s * A - delta - n = 0.05
When a question asks "in which year does something first happen, within a limit?", the loop tests each year before stepping, and the answer when it never happens is None. Test the result with is None: a plain if result: would also treat year 0 as "never".
def first_year_reaching(target, y0, growth, max_years):
y = y0
for t in range(max_years + 1):
if y >= target: # year t is tested before stepping to t + 1
return t
y = y * (1 + growth)
return None
print(first_year_reaching(3, 2, 0.05, 50), first_year_reaching(3, 2, 0.05, 5),
first_year_reaching(3, 2, -0.01, 1000), first_year_reaching(1, 2, 0.05, 0))
result = first_year_reaching(3, 2, 0.05, 5)
print("not within the limit" if result is None else result)
9 None None 0
not within the limit
Common mistakes: leaving g out of the break-even term. Reporting output per effective worker as output per worker (multiply by E_t). Using E_{t+1} for year t. Stepping before testing, which skips year 0. Looping to max_years - 1. Using >= where the problem says "strictly greater".
Practice: Better Tools Every Year, The Island That Never Slows Down
How fast is convergence? Linearising and the half-life
Any rule x_{t+1} = f(x_t) with a fixed point x^* = f(x^*) can be studied the same way. Close to x^* the curve f is almost its tangent line, the line through (x^*, x^*) whose slope is the slope of f there, written \rho = f'(x^*):
Near the fixed point the gap shrinks by the factor \rho each year (if 0 < \rho < 1), exactly like Level 2's output catching up. After t years a small gap is \rho^t times what it was, and the half-life, the time to close half the gap, solves \rho^h = 1/2. Take logarithms of both sides: h \ln \rho = \ln \tfrac12, so
(any base of logarithm works, as long as both use the same one).
A fish stock in a lake grows by 0.3\,x\,(1 - x/1000) a year (fast when the stock is small, slowly near the lake's capacity of 1000 tonnes), and fishers take 10 % of it:
Term by term with the power rule, f'(x) = 1 + 0.3 - 0.0006\,x - 0.1. A term with a fractional power, such as c\,x^{0.4}, is no harder: it gives 0.4\,c\,x^{-0.6}.
import math
r, C, m = 0.3, 1000, 0.1 # growth, capacity, share caught each year
f = lambda x: x + r * x * (1 - x / C) - m * x
fprime = lambda x: 1 + r - 2 * r * x / C - m
x_star = C * (1 - m / r) # where growth r (1 - x / C) equals the catch m
rho = fprime(x_star)
step = 1e-6
print("x* =", round(x_star, 4), " f(x*) =", round(f(x_star), 4))
print("rho =", round(rho, 6), " from a tiny step:", round((f(x_star + step) - f(x_star)) / step, 6))
h = math.log(0.5) / math.log(rho)
print("predicted half-life", round(h, 4), " check:", round(rho ** h, 6))
for x0 in (600, 100, 1000, 1500):
gap0, x, t = abs(x0 - x_star), x0, 0
while abs(x - x_star) > 0.5 * gap0:
x = f(x)
t += 1
print("from", x0, ": half the gap closed in year", t)
x* = 666.6667 f(x*) = 666.6667
rho = 0.8 from a tiny step: 0.8
predicted half-life 3.1063 check: 0.5
from 600 : half the gap closed in year 4
from 100 : half the gap closed in year 11
from 1000 : half the gap closed in year 3
from 1500 : half the gap closed in year 2
The gap shrinks by about 20 % a year near the fixed point, and the predicted half-life is 3.1 years. Starting close (600 tonnes), the simulation first has half the gap closed in year 4, the first whole year after 3.1. Far from the fixed point the straight line misleads. This f curves downwards (it is concave: the -0.0003\,x^2 term), so it lies below its tangent line. From below, each year closes less of the gap than \rho promises: from 100 tonnes it takes 11 years. From above, each year closes more: 3 years from 1000, 2 from 1500. The linear prediction is too fast from below and too slow from above. A Solow rule with diminishing returns is concave too. A \rho close to 1 means slow convergence.
Common mistakes: differentiating only one term and forgetting the 1 that comes from the x term. Evaluating the slope at the starting point instead of at the fixed point. Mixing math.log10 and math.log. Measuring the half gap without absolute values when starting above the fixed point. Rounding the prediction.
Practice: Halfway Home: Formula Against Simulation
Practice
These problems are required on this path at this level; the path adds one problem from each of the level's topics, picked for you and kept.
- Two Stations, One Evening Slot
- The Price War Between Two Bakeries
- Two Mills, One Flour Market
- The Fishing Fleet, Year by Year
- Where Saving Just Covers the Wear
- Two Valleys, One Destination
Checklist
You can now:
- Read a payoff table
payoff[i][j] = (row's, column's), find best responses down a column and across a row, and keep every tie. - Test a cell for a pure-strategy Nash equilibrium (a tie is not a gain), list equilibria in row-major order, and recognise a prisoners' dilemma with totals and Pareto domination.
- Remove strictly dominated strategies repeatedly over what is left, and tell strict from weak dominance.
- Find a mixed-strategy equilibrium from indifference, each player's probability from the other's payoffs, in exact fractions.
- Derive a Cournot best response under linear demand, solve the symmetric and the unequal-cost equilibrium, and handle a firm that is shut out.
- Check a Bertrand price profile by trying every unilateral deviation in whole cents, and solve a Stackelberg game backwards, limit output included.
- Solve a game tree by backward induction with a recursive function over nested dicts, with a choice at every node, and simulate repeated games from strategies that read the history.
- Compute output per worker and its marginal product with the power rule, simulate the Solow rule with this year's capital, and explain why n joins \delta.
- Solve for the steady state with fractional exponents, find golden-rule consumption, and explain why saving raises the level of income but not its long-run growth.
- Count years to convergence with a
whileloop that tests year 0 first, rank economies by conditional convergence, split growth into inputs and a residual, use effective workers and the AK model, and predict a half-life by linearising.