Skip to content

Commit bfc0de8

Browse files
committed
updates
1 parent 7ca9513 commit bfc0de8

File tree

1 file changed

+63
-44
lines changed

1 file changed

+63
-44
lines changed

lectures/gorman_heterogeneous_households.md

Lines changed: 63 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ import matplotlib.pyplot as plt
5353

5454
Gorman aggregation lets us solve heterogeneous-household economies in two steps: solve a representative-agent linear-quadratic planning problem for aggregates, then recover household allocations via a "sharing rule" with household-specific deviation terms.
5555

56-
Gorman conditions ensure all consumers have parallel Engel curves, so aggregate allocations and prices can be determined independently of distribution.
56+
The key is that Gorman conditions ensure all consumers have parallel Engel curves, so aggregate allocations and prices can be determined independently of distribution.
5757

5858
This eliminates the standard problem where the utility possibility frontier shifts with endowment changes, making it impossible to rank allocations without specifying distributional weights.
5959

@@ -254,6 +254,8 @@ In the dynamic setting, we set the utility index $u^j$ equal to household $j$'s
254254
255255
The ratio $\mu_{0j}^w / \mu_{0a}^w$ (where $\mu_{0a}^w = \sum_j \mu_{0j}^w$) then serves as the time-invariant Gorman weight that determines household $j$'s share of aggregate consumption in excess of baseline.
256256
257+
Without spoiling further, we now introduce the basic setup for the dynamic heterogeneous-household economy.
258+
257259
258260
## Set up
259261
@@ -290,6 +292,8 @@ The vector $z_t$ typically contains three types of components.
290292
The selection matrices $U_b$ and $U_d$ pick out which components of $z_t$ affect
291293
household preferences (bliss points) and endowments.
292294
295+
This structure allows us to model both aggregate and idiosyncratic shocks within a unified framework at a later stage.
296+
293297
### Technologies
294298
295299
The economy's resource constraint is
@@ -946,7 +950,11 @@ This transforms heterogeneous endowment risk into proportional shares of aggrega
946950
947951
We now derive the law of motion for the bond position $\hat{k}_{jt}$.
948952
949-
First, write the budget constraint. Household $j$'s time-$t$ resources equal uses:
953+
First, write the budget constraint.
954+
955+
Household $j$'s time-$t$ resources are its mutual fund dividend $\mu_j d_t$ plus the return on its bond position $R(\mu_j k_{t-1} + \hat{k}_{j,t-1})$.
956+
957+
Its uses of funds are consumption $c_{jt}$ plus the new bond position $(\mu_j k_t + \hat{k}_{jt})$:
950958
951959
$$
952960
\underbrace{\mu_j d_t}_{\text{mutual fund dividend}} + \underbrace{R(\mu_j k_{t-1} + \hat{k}_{j,t-1})}_{\text{bond return}} = \underbrace{c_{jt}}_{\text{consumption}} + \underbrace{(\mu_j k_t + \hat{k}_{jt})}_{\text{new bonds}}
@@ -1012,7 +1020,7 @@ $$
10121020
10131021
This is the present value of future deviation consumption.
10141022
1015-
Under the Chapter 12.6 measurability restriction ($\tilde{\chi}_{jt} \in \mathcal{J}_0$), this sum is known at date zero and can be used to set the initial bond position.
1023+
Under the measurability restriction ($\tilde{\chi}_{jt} \in \mathcal{J}_0$), this sum is known at date zero and can be used to set the initial bond position.
10161024
10171025
```{note}
10181026
@@ -1030,7 +1038,9 @@ All changes over time in portfolio composition take place through transactions i
10301038
Below is the code that computes household allocations and limited-markets portfolios along a fixed aggregate path according to the mechanism described above
10311039
10321040
```{code-cell} ipython3
1033-
def compute_household_paths(econ, U_b_list, U_d_list, x0, x_path, γ_1, Λ, h0i=None, k0i=None):
1041+
def compute_household_paths(
1042+
econ, U_b_list, U_d_list, x0, x_path,
1043+
γ_1, Λ, h0i=None, k0i=None):
10341044
"""
10351045
Compute household allocations and limited-markets portfolios
10361046
along a fixed aggregate path.
@@ -1131,7 +1141,8 @@ def compute_household_paths(econ, U_b_list, U_d_list, x0, x_path, γ_1, Λ, h0i=
11311141
# At t=0, assume η_{-1} = 0
11321142
χ_tilde[j, 0] = (Π_inv @ b_tilde[:, 0]).squeeze()
11331143
for t in range(1, T):
1134-
χ_tilde[j, t] = (-Π_inv @ Λ @ η[:, t - 1] + Π_inv @ b_tilde[:, t]).squeeze()
1144+
χ_tilde[j, t] = (
1145+
-Π_inv @ Λ @ η[:, t - 1] + Π_inv @ b_tilde[:, t]).squeeze()
11351146
η[:, t] = (A_h @ η[:, t - 1] + B_h @ b_tilde[:, t]).squeeze()
11361147
11371148
c_j[j] = (μ[j] * c[0] + χ_tilde[j]).squeeze()
@@ -1179,7 +1190,15 @@ def compute_household_paths(econ, U_b_list, U_d_list, x0, x_path, γ_1, Λ, h0i=
11791190
```
11801191
11811192
The next function collects everything to solve the planner's problem and compute household paths
1182-
into one function
1193+
into one function.
1194+
1195+
We first use the `DLE` class to set up the representative-agent DLE problem.
1196+
1197+
Then we build the full initial state by stacking zeros for lagged durables and capital with the initial exogenous state.
1198+
1199+
Using the `LQ` class, we solve the LQ problem and simulate paths for the full state.
1200+
1201+
Finally, we call `compute_household_paths` to get household allocations and limited-markets portfolios along the simulated path
11831202
11841203
```{code-cell} ipython3
11851204
def solve_model(info, tech, pref, U_b_list, U_d_list, γ_1, Λ, z0, ts_length=2000):
@@ -1462,14 +1481,16 @@ def build_reverse_engineered_gorman_extended(
14621481
allowing the full heterogeneous dynamics to be captured.
14631482
14641483
The state vector is:
1465-
z_t = [1, d_{a,t}, d_{a,t-1}, eta_{n_absorb+1,t}, ..., eta_{n,t}, xi_{1,t}, ..., xi_{n,t}]
1484+
z_t = [1, d_{a,t}, d_{a,t-1}, eta_{n_absorb+1,t},
1485+
..., eta_{n,t}, xi_{1,t}, ..., xi_{n,t}]
14661486
1467-
The first n_absorb households absorb the negative sum of all idiosyncratic shocks
1468-
to ensure shocks sum to zero:
1469-
sum_{j=1}^{n_absorb} (-1/n_absorb * sum_{k>n_absorb} eta_k) + sum_{k>n_absorb} eta_k = 0
1487+
The first n_absorb households absorb the negative sum
1488+
of all idiosyncratic shocks to ensure shocks sum to zero:
1489+
sum_{j=1}^{n_absorb} (-1/n_absorb * sum_{k>n_absorb} eta_k)
1490+
+ sum_{k>n_absorb} eta_k = 0
14701491
1471-
Each household k > n_absorb has its own idiosyncratic endowment shock eta_{k,t}
1472-
following an AR(1):
1492+
Each household k > n_absorb has its own idiosyncratic endowment shock
1493+
eta_{k,t} following an AR(1):
14731494
14741495
eta_{k,t+1} = rho_idio[k] * eta_{k,t} + sigma_k * w_{k,t+1}
14751496
@@ -1492,13 +1513,7 @@ def build_reverse_engineered_gorman_extended(
14921513
if n_absorb < 1 or n_absorb >= n:
14931514
raise ValueError(f"n_absorb must be in [1, n-1], got {n_absorb} with n={n}")
14941515
1495-
# State vector:
1496-
# z_t = [1, d_{a,t}, d_{a,t-1}, eta_{n_absorb+1,t}, ..., eta_{n,t}, xi_{1,t}, ..., xi_{n,t}]
1497-
# where eta_{j,t} are idiosyncratic endowment shocks (j=n_absorb+1..n)
1498-
# and xi_{j,t} are preference shocks (j=1..n)
1499-
# First n_absorb households absorb -1/n_absorb * sum(eta_j) to ensure shocks sum to zero
1500-
# Dimension: 3 + (n - n_absorb) + n = 2n + 3 - n_absorb
1501-
1516+
# Dimensions
15021517
n_idio = n - n_absorb # eta_{n_absorb+1}, ..., eta_n
15031518
n_pref = n # xi_1, ..., xi_n
15041519
nz = 3 + n_idio + n_pref
@@ -1580,7 +1595,7 @@ def build_reverse_engineered_gorman_extended(
15801595
15811596
### 100-household reverse engineered economy
15821597
1583-
We now instantiate a 100-household economy using the reverse-engineered Gorman specification.
1598+
We now instantiate a 100-household economy using the setup above.
15841599
15851600
We use the same technology and preference parameters as the two-household example.
15861601
@@ -1679,7 +1694,7 @@ paths, econ = solve_model(info_ar1, tech_ar1, pref_ar1,
16791694
```{code-cell} ipython3
16801695
print(f"State dimension: {A22.shape[0]}, shock dimension: {C2.shape[1]}")
16811696
print(f"Aggregate: ρ_1={ρ1:.3f}, ρ_2={ρ2:.3f}, σ_a={σ_a:.2f}")
1682-
print(f"Endowments: α in [{np.min(αs):.2f}, {np.max(αs):.2f}], Σφ={np.sum(φs):.6f}")
1697+
print(f"Endowments: α in [{np.min(αs):.2f}, {np.max(αs):.2f}]")
16831698
```
16841699
16851700
The next plots show household consumption and dividend paths after discarding the initial burn-in period.
@@ -1705,7 +1720,9 @@ plt.show()
17051720
17061721
### Closed-loop state-space system
17071722
1708-
The DLE framework represents the economy as a linear state-space system. After solving the optimal control problem and substituting the policy rule, we obtain a closed-loop system:
1723+
The DLE framework represents the economy as a linear state-space system.
1724+
1725+
After solving the optimal control problem and substituting the policy rule, we obtain a closed-loop system:
17091726
17101727
$$
17111728
x_{t+1} = A_0 x_t + C w_{t+1},
@@ -1762,9 +1779,6 @@ G = np.vstack([
17621779
n_h = np.atleast_2d(econ.thetah).shape[0]
17631780
n_k = np.atleast_2d(econ.thetak).shape[0]
17641781
n_endo = n_h + n_k # endogenous state dimension
1765-
1766-
print(f"Shapes: A0 {A0.shape}, C {C.shape}, G {G.shape}")
1767-
print(f"max |A0[{n_endo}:,{n_endo}:] - A22| = {np.max(np.abs(A0[n_endo:, n_endo:] - A22)):.2e}")
17681782
```
17691783
17701784
With the state space representation, we can compute impulse responses to show how shocks propagate through the economy.
@@ -1834,7 +1848,6 @@ This causes capital to rise as $d_{a,t}$ falls — this is permanent income logi
18341848
Next, we examine if the household consumption and endowment paths generated by the simulation obey the Gorman sharing rule
18351849
18361850
```{code-cell} ipython3
1837-
# Now stack the household consumption panel with household endowments and rerun DMD.
18381851
c_j_t0 = paths["c_j"][..., t0:]
18391852
d_j_t0 = paths["d_share"][..., t0:]
18401853
@@ -1854,15 +1867,16 @@ n_to_plot = 1
18541867
fig, axes = plt.subplots(2, 1, figsize=(14, 8))
18551868
18561869
# Top panel: Aggregate endowment
1857-
axes[0].plot(time_idx, d_agg[:T_plot], linewidth=2.5, color='C0', label='Aggregate endowment $d_t$')
1870+
axes[0].plot(time_idx, d_agg[:T_plot], linewidth=2.5, color='C0',
1871+
label='Aggregate endowment $d_t$')
18581872
axes[0].set_ylabel('Endowment')
1859-
axes[0].set_title('Aggregate Endowment (contains ghost AR(2) process)')
1873+
axes[0].set_title('Aggregate Endowment')
18601874
axes[0].legend()
18611875
18621876
# Also plot the mean across households
18631877
d_mean = d_households[:, :T_plot].mean(axis=0)
18641878
axes[1].plot(time_idx, d_mean, linewidth=2.5, color='black', linestyle='--',
1865-
label=f'Mean across {d_households.shape[0]} households', alpha=0.8)
1879+
label=f'Mean across {d_households.shape[0]} households', alpha=0.8)
18661880
18671881
axes[1].set_xlabel('Time (after burn-in)')
18681882
axes[1].set_ylabel('Endowment')
@@ -1912,7 +1926,9 @@ To implement a redistribution from $\mu$ to $\lambda^*$, we construct new Pareto
19121926
3. Leave middle-$j$ types relatively unaffected
19131927
4. Preserve the constraint $\sum_{j=1}^J \lambda_j^* = 1$
19141928
1915-
We implement this using a smooth transformation. Let $\{\lambda_j\}_{j=1}^J$ denote the original competitive equilibrium Pareto weights (sorted in descending order). Define the redistribution function:
1929+
We implement this using a smooth transformation.
1930+
1931+
Let $\{\lambda_j\}_{j=1}^J$ denote the original competitive equilibrium Pareto weights (sorted in descending order). Define the redistribution function:
19161932
19171933
$$
19181934
f(j; J) = \frac{j-1}{J-1}, \qquad
@@ -1922,8 +1938,8 @@ $$
19221938
19231939
where:
19241940
- $j \in \{1, \ldots, J\}$ is the household index
1925-
- $\alpha > 0$ controls the overall magnitude of redistribution (with $\tau$ maximized at the extremes)
1926-
- $\beta > 1$ controls the progressivity (higher $\beta$ concentrates redistribution more strongly in the tails)
1941+
- $\alpha > 0$ controls the overall magnitude of redistribution
1942+
- $\beta$ controls the progressivity (higher $\beta$ concentrates redistribution more strongly in the tails)
19271943
19281944
The redistributed Pareto weights are:
19291945
@@ -1951,7 +1967,7 @@ def create_redistributed_weights(λ_orig, α=0.5, β=2.0):
19511967
λ_bar = 1.0 / J
19521968
j_indices = np.arange(J)
19531969
f_j = j_indices / (J - 1)
1954-
dist_from_median = np.abs(f_j - 0.5) / 0.5 # in [0, 1], smallest near the median
1970+
dist_from_median = np.abs(f_j - 0.5) / 0.5
19551971
τ_j = np.clip(α * (dist_from_median ** β), 0.0, 1.0)
19561972
λ_tilde = λ_orig + τ_j * (λ_bar - λ_orig)
19571973
λ_star = λ_tilde / λ_tilde.sum()
@@ -1977,10 +1993,12 @@ red_β = 0.0
19771993
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
19781994
19791995
n_plot = len(λ_orig_sorted)
1980-
axes[0].plot(λ_orig_sorted[:n_plot], 'o-', label=r'original $\lambda_j$', alpha=0.7, lw=2)
1981-
axes[0].plot(λ_star_sorted[:n_plot], 's-', label=r'redistributed $\lambda_j^*$', alpha=0.7, lw=2)
1996+
axes[0].plot(λ_orig_sorted[:n_plot], 'o-',
1997+
label=r'original $\lambda_j$', alpha=0.7, lw=2)
1998+
axes[0].plot(λ_star_sorted[:n_plot], 's-',
1999+
label=r'redistributed $\lambda_j^*$', alpha=0.7, lw=2)
19822000
axes[0].axhline(1.0 / len(λ_orig_sorted), color='k', linestyle='--',
1983-
label=f'equal weight (1/{len(λ_orig_sorted)})', alpha=0.5, lw=2)
2001+
label=f'equal weight (1/{len(λ_orig_sorted)})', alpha=0.5, lw=2)
19842002
axes[0].set_xlabel(r'household index $j$ (sorted by $\lambda$)')
19852003
axes[0].set_ylabel('Pareto weight')
19862004
axes[0].legend()
@@ -2050,7 +2068,8 @@ def allocation_from_weights(paths, econ, U_b_list, weights, γ_1, Λ, h0i=None):
20502068
# At t=0, assume η_{-1} = 0
20512069
χ_tilde[j, 0] = (Π_inv @ b_tilde[:, 0]).squeeze()
20522070
for t in range(1, T):
2053-
χ_tilde[j, t] = (-Π_inv @ Λ @ η[:, t - 1] + Π_inv @ b_tilde[:, t]).squeeze()
2071+
χ_tilde[j, t] = (
2072+
-Π_inv @ Λ @ η[:, t - 1] + Π_inv @ b_tilde[:, t]).squeeze()
20542073
η[:, t] = (A_h @ η[:, t - 1] + B_h @ b_tilde[:, t]).squeeze()
20552074
20562075
c_j[j] = (weights[j] * c_agg[0] + χ_tilde[j]).squeeze()
@@ -2071,7 +2090,9 @@ def allocation_from_weights(paths, econ, U_b_list, weights, γ_1, Λ, h0i=None):
20712090
# Net income: dividend share + asset return
20722091
y_net = weights[:, None] * d_agg[0, :] + (R - 1) * a_lag
20732092
2074-
return {"c": c_j, "y_net": y_net, "χ_tilde": χ_tilde, "k_hat": k_hat, "a_total": a_total}
2093+
return {"c": c_j, "y_net": y_net,
2094+
"χ_tilde": χ_tilde, "k_hat": k_hat,
2095+
"a_total": a_total}
20752096
```
20762097
20772098
```{code-cell} ipython3
@@ -2090,14 +2111,12 @@ idx_sorted = np.argsort(-μ_values)
20902111
λ_star = np.empty_like(μ_values, dtype=float)
20912112
λ_star[idx_sorted] = λ_star_sorted
20922113
2093-
print(f"Weight redistribution:")
2094-
print(f" std(μ): {np.std(μ_values):.4f} → std(λ*): {np.std(λ_star):.4f}")
2095-
print(f" p90/p10: {np.percentile(μ_values, 90)/np.percentile(μ_values, 10):.2f} → {np.percentile(λ_star, 90)/np.percentile(λ_star, 10):.2f}")
2096-
20972114
h0i_alloc = np.array([[0.0]])
20982115
2099-
pre = allocation_from_weights(paths, econ, Ub_list, μ_values, γ_1, Λ, h0i_alloc)
2100-
post = allocation_from_weights(paths, econ, Ub_list, λ_star, γ_1, Λ, h0i_alloc)
2116+
pre = allocation_from_weights(paths, econ,
2117+
Ub_list, μ_values, γ_1, Λ, h0i_alloc)
2118+
post = allocation_from_weights(paths, econ,
2119+
Ub_list, λ_star, γ_1, Λ, h0i_alloc)
21012120
21022121
c_pre = pre["c"][:, t0:]
21032122
y_pre = pre["y_net"][:, t0:]

0 commit comments

Comments
 (0)