diff --git a/lectures/gorman_heterogeneous_households.md b/lectures/gorman_heterogeneous_households.md index f73f0e6a..33df7705 100644 --- a/lectures/gorman_heterogeneous_households.md +++ b/lectures/gorman_heterogeneous_households.md @@ -1203,7 +1203,7 @@ def compute_household_paths( # Original endowment claim (before trading) d_j[j] = (U_dj @ z_path)[0, :] - # Dividend income under limited markets: μ_j × aggregate endowment + # Dividend income under limited markets: μ_j aggregate endowment d_share[j] = (μ[j] * d[0]).squeeze() # Capital share in mutual fund @@ -1217,7 +1217,7 @@ def compute_household_paths( a_total[j] = k_share[j] + k_hat[j] - # Validate that Pareto weights sum to 1 (required for sharing rule consistency) + # Validate that Gorman weights sum to 1 μ_sum = np.sum(μ) if abs(μ_sum - 1.0) > 1e-6: import warnings @@ -1569,7 +1569,8 @@ def build_gorman_extended( n_absorb = max(1, n // 10) n_absorb = int(n_absorb) if n_absorb < 1 or n_absorb >= n: - raise ValueError(f"n_absorb must be in [1, n-1], got {n_absorb} with n={n}") + raise ValueError( + f"n_absorb must be in [1, n-1], got {n_absorb} with n={n}") # Dimensions n_idio = n - n_absorb # eta_{n_absorb+1}, ..., eta_n @@ -1634,12 +1635,17 @@ def build_gorman_extended( Ud = sum(Ud_per_house) # Ub_per_house: bliss loading - # b_{jt} = b_bar + xi_{j,t} + # b_{jt} = b_bars[j] + xi_{j,t} + # b_bar can be scalar (same for all) or array (household-specific) + b_bars = np.atleast_1d(b_bar) + if b_bars.size == 1: + b_bars = np.full(n, float(b_bars[0])) + Ub_per_house = [] for j in range(n): row = np.zeros((1, nz)) - row[0, 0] = b_bar # constant bliss - row[0, 3 + n_idio + j] = 1.0 # loading on xi_j + row[0, 0] = b_bars[j] # household-specific bliss + row[0, 3 + n_idio + j] = 1.0 # loading on xi_j (zeroed if gammas=0) Ub_per_house.append(row) Ub = sum(Ub_per_house) @@ -1649,6 +1655,8 @@ def build_gorman_extended( x0[0, 0] = 1.0 # constant = 1 return A22, C2, Ub, Ud, Ub_per_house, Ud_per_house, x0 + + ``` ### 100-household economy @@ -1704,11 +1712,13 @@ n_absorb = 50 # Poorer households face larger, more persistent idiosyncratic shocks σ_idio_min, σ_idio_max = 0.2, 5.0 σs = σ_idio_min + (σ_idio_max - σ_idio_min) * (poorness ** 2.0) -ρ_idio_min, ρ_idio_max = 0.0, 0.98 +ρ_idio_min, ρ_idio_max = 0.0, 0.94 ρ_idio = ρ_idio_min + (ρ_idio_max - ρ_idio_min) * (poorness[n_absorb:] ** 1.0) # Preference shocks are muted -b_bar = 5.0 +# Bliss points scale with endowment so high-α households want more consumption +b_bar_base = 5.0 +b_bars = b_bar_base * (αs / αs.mean()) # household-specific bliss points γs_pref = np.zeros(N) ρ_pref = 0.0 @@ -1716,12 +1726,13 @@ A22, C2, Ub, Ud, Ub_list, Ud_list, x0 = build_gorman_extended( n=N, rho1=ρ1, rho2=ρ2, sigma_a=σ_a, alphas=αs, phis=φs, sigmas=σs, - b_bar=b_bar, gammas=γs_pref, + b_bar=b_bars, gammas=γs_pref, rho_idio=ρ_idio, rho_pref=ρ_pref, n_absorb=n_absorb, ) print(f"sum(φs) = {np.sum(φs):.6f} (should be 1.0)") + ``` ```{code-cell} ipython3 @@ -2069,7 +2080,9 @@ Once we have the redistributed Pareto weights, we can compute the new household The function below computes household consumption and income under given Pareto weights ```{code-cell} ipython3 -def allocation_from_weights(paths, econ, U_b_list, weights, γ_1, Λ, h0i=None): +def allocation_from_weights(paths, econ, + U_b_list, weights, γ_1, Λ, h0i=None, + original_weights=None): """ Compute household consumption and income under given Pareto weights. """ @@ -2077,6 +2090,12 @@ def allocation_from_weights(paths, econ, U_b_list, weights, γ_1, Λ, h0i=None): weights = np.asarray(weights).reshape(-1) N = len(weights) + # For χ computation, use original weights if provided + if original_weights is None: + weights_for_chi = weights + else: + weights_for_chi = np.asarray(original_weights).reshape(-1) + # Extract aggregate paths x_path = paths["x_path"] c_agg = paths["c"] @@ -2111,7 +2130,8 @@ def allocation_from_weights(paths, econ, U_b_list, weights, γ_1, Λ, h0i=None): for j in range(N): U_bj = np.asarray(U_b_list[j], dtype=float) b_agg = econ.Sb @ x_path - b_tilde = U_bj @ z_path - weights[j] * b_agg + # Use weights_for_chi for b_tilde: keeps χ fixed during redistribution + b_tilde = U_bj @ z_path - weights_for_chi[j] * b_agg η = np.zeros((n_h, T + 1)) η[:, 0] = np.asarray(h0i).reshape(-1) @@ -2163,10 +2183,14 @@ idx_sorted = np.argsort(-μ_values) h0i_alloc = np.array([[0.0]]) -pre = allocation_from_weights(paths, econ, +pre = allocation_from_weights(paths, econ, Ub_list, μ_values, γ_1, Λ, h0i_alloc) -post = allocation_from_weights(paths, econ, - Ub_list, λ_star, γ_1, Λ, h0i_alloc) + +# For post-redistribution: use λ_star for proportional share, but keep χ fixed +# that is to say preferences don't change with taxes +post = allocation_from_weights(paths, econ, + Ub_list, λ_star, γ_1, Λ, h0i_alloc, + original_weights=μ_values) c_pre = pre["c"][:, t0:] y_pre = pre["y_net"][:, t0:] @@ -2184,31 +2208,40 @@ y_post_pct = _pct(y_post[:, :T_ts]) c_pre_pct = _pct(c_pre[:, :T_ts]) c_post_pct = _pct(c_post[:, :T_ts]) -fig, ax = plt.subplots(1, 3, figsize=(14, 4), sharey=True) +fig, ax = plt.subplots(1, 3, figsize=(14, 6), sharey=True) ax[0].plot(t, y_pre_pct[0], label="p90", lw=2) ax[0].plot(t, y_pre_pct[1], label="p50", lw=2) ax[0].plot(t, y_pre_pct[2], label="p10", lw=2) -ax[0].set_xlabel("time") -ax[0].set_ylabel(r"pre-tax income $y^{pre}$") -ax[0].legend() +ax[0].set_xlabel("$t$", fontsize=20) +ax[0].set_ylabel(r"$y^{pre}$", fontsize=20) ax[1].plot(t, y_post_pct[0], label="p90", lw=2) ax[1].plot(t, y_post_pct[1], label="p50", lw=2) ax[1].plot(t, y_post_pct[2], label="p10", lw=2) -ax[1].set_xlabel("time") -ax[1].set_ylabel(r"post-tax income $y^{post}$") -ax[1].legend() +ax[1].set_xlabel("$t$") +ax[1].set_ylabel(r"$y^{post}$", fontsize=20) ax[2].plot(t, c_post_pct[0], label="p90", lw=2) ax[2].plot(t, c_post_pct[1], label="p50", lw=2) ax[2].plot(t, c_post_pct[2], label="p10", lw=2) -ax[2].set_xlabel("time") -ax[2].set_ylabel(r"consumption $c^{post}$") -ax[2].legend() +ax[2].set_xlabel("$t$") +ax[2].set_ylabel(r"$c^{post}$", fontsize=20) + +handles, labels = ax[0].get_legend_handles_labels() + +fig.legend( + handles, + labels, + loc="upper center", + ncol=3, + frameon=False, + bbox_to_anchor=(0.5, 1.1), +) plt.tight_layout() plt.show() + ``` The figures reveal a striking reduction in income and consumption inequality after redistribution.