75. 收入波动问题 V:资产随机收益#

GPU

本讲座是在配有GPU的机器上构建的——不过没有GPU也可以运行。

Google Colab 提供带GPU的免费套餐,使用方法如下:

  1. 点击右上角的”播放”图标

  2. 选择 Colab

  3. 将运行时环境设置为包含GPU

除了 Anaconda 中的内容外,本讲座还需要以下库:

!pip install quantecon jax

Hide code cell output

Requirement already satisfied: quantecon in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (0.11.4)
Requirement already satisfied: jax in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (0.11.1)
Requirement already satisfied: numba>=0.49.0 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from quantecon) (0.65.1)
Requirement already satisfied: numpy>=1.17.0 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from quantecon) (2.4.6)
Requirement already satisfied: requests in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from quantecon) (2.34.2)
Requirement already satisfied: scipy>=1.5.0 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from quantecon) (1.18.0)
Requirement already satisfied: sympy in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from quantecon) (1.14.0)
Requirement already satisfied: jaxlib<=0.11.1,>=0.11.1 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from jax) (0.11.1)
Requirement already satisfied: ml_dtypes>=0.5.0 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from jax) (0.6.0)
Requirement already satisfied: opt_einsum in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from jax) (3.4.0)
Requirement already satisfied: llvmlite<0.48,>=0.47.0dev0 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from numba>=0.49.0->quantecon) (0.47.0)
Requirement already satisfied: charset_normalizer<4,>=2 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from requests->quantecon) (3.4.7)
Requirement already satisfied: idna<4,>=2.5 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from requests->quantecon) (3.18)
Requirement already satisfied: urllib3<3,>=1.26 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from requests->quantecon) (2.7.0)
Requirement already satisfied: certifi>=2023.5.7 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from requests->quantecon) (2026.6.17)
Requirement already satisfied: mpmath<1.4,>=1.1.0 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from sympy->quantecon) (1.3.0)

75.1. 概述#

在本讲座中,我们继续研究 收入波动问题 III:内生网格法 中描述的收入波动问题。

之前假设利率是固定的,但现在我们允许资产收益随状态变化。

这符合大多数拥有正资产的家庭面临资本收入风险这一事实。

有人认为,建模资本收入风险对于理解收入和财富的联合分布至关重要(参见,例如,[Benhabib et al., 2015][Stachurski and Toda, 2019])。

本文提出的家庭储蓄模型的理论性质在 [Ma et al., 2020] 中有详细分析。

在计算方面,我们结合时间迭代和内生网格方法来快速准确地求解模型。

我们需要以下导入:

import matplotlib.pyplot as plt
import matplotlib as mpl
FONTPATH = "fonts/SourceHanSerifSC-SemiBold.otf"
mpl.font_manager.fontManager.addfont(FONTPATH)
plt.rcParams['font.family'] = ['Source Han Serif SC']
import numpy as np
import quantecon as qe
import jax
import jax.numpy as jnp
from jax import vmap
from typing import NamedTuple
from functools import partial

75.2. 模型#

在本节中,我们回顾家庭问题及其最优性结果。

75.2.1. 设定#

家庭选择消费-资产路径 \(\{(c_t, a_t)\}\) 以最大化

(75.1)#\[\mathbb E \left\{ \sum_{t=0}^\infty \beta^t u(c_t) \right\}\]

受约束于

(75.2)#\[a_{t+1} = R_{t+1} (a_t - c_t) + Y_{t+1} \; \text{ 且 } \; 0 \leq c_t \leq a_t,\]

初始条件 \((a_0, Z_0)=(a,z)\) 视为给定。

The Income Fluctuation Problem IV: Transient Income Shocks 唯一的不同之处在于,财富的总收益率 \(\{R_t\}_{t \geq 1}\) 现在允许是随机的。

具体而言,我们假设

(75.3)#\[ R_t = R(Z_t, \zeta_t) \quad \text{且} \quad Y_t = Y(Z_t, \eta_t),\]

其中

  • \(R\)\(Y\) 是时不变的非负函数,

  • 创新过程 \(\{\zeta_t\}\)\(\{\eta_t\}\) 独立同分布且相互独立,

  • \(\{Z_t\}_{t \geq 0}\) 是有限集 \(\mathsf Z\) 上的马尔可夫链

\(P\) 表示链 \(\{Z_t\}_{t \geq 0}\) 的马尔可夫矩阵。

在下文中,\(\mathbb E_z \hat X\) 表示给定当前值 \(Z = z\) 时下一期值 \(\hat X\) 的期望。

75.2.2. 假设#

我们需要一些限制条件来确保目标 (75.1) 是有限的,并且下面描述的解法能够收敛。

我们还需要确保财富的现值不会增长得太快。

\(\{R_t\}\) 是常数时,我们要求 \(\beta R < 1\)

现在它是随机的,我们要求(参见 [Ma et al., 2020]

(75.4)#\[\beta G_R < 1, \quad \text{其中} \quad G_R := \lim_{n \to \infty} \left(\mathbb E \prod_{t=1}^n R_t \right)^{1/n}\]

\(G_R\) 可以理解为长期(几何)平均总收益率。

为了简化本讲座,我们将假设利率过程是独立同分布的

在这种情况下,从 \(G_R\) 的定义可以清楚地看出 \(G_R\) 就是 \(\mathbb E R_t\)

我们在下面的代码中检验条件 \(\beta \mathbb E R_t < 1\)

最后,我们对非金融收入施加一些常规的技术性限制。

\[ \mathbb E \, Y_t < \infty \text{ 且 } \mathbb E \, u'(Y_t) < \infty \label{a:y0} \]

一个相对简单且满足所有这些限制的环境是 [Benhabib et al., 2015] 的独立同分布和 CRRA 环境。

75.2.3. 最优性#

令候选消费政策类 \(\mathscr C\) 的定义如 收入波动问题 III:内生网格法 中所述。

[Ma et al., 2020] 中证明,在所述假设下,

  • 任何满足欧拉方程的 \(\sigma \in \mathscr C\) 都是最优政策,且

  • \(\mathscr C\) 中恰好存在一个这样的政策。

在当前设定中,欧拉方程的形式为

(75.5)#\[(u' \circ \sigma) (a, z) = \max \left\{ \beta \, \mathbb E_z \,\hat{R} \, (u' \circ \sigma)[\hat{R}(a - \sigma(a, z)) + \hat{Y}, \, \hat{Z}], \, u'(a) \right\}\]

(直觉和推导与 收入波动问题 III:内生网格法 中的内容类似。)

我们再次使用时间迭代来求解欧拉方程,使用与欧拉方程 (75.5) 相匹配的 Coleman–Reffett 算子 \(K\) 进行迭代。

75.3. 求解算法#

75.3.1. 时间迭代算子#

我们对候选类 \(\sigma \in \mathscr C\) 消费政策的定义与 收入波动问题 III:内生网格法 中的定义相同。

对于固定的 \(\sigma \in \mathscr C\)\((a,z) \in \mathbf S\),函数 \(K\sigma\)\((a,z)\) 处的值 \(K\sigma(a,z)\) 定义为满足以下方程的 \(\xi \in (0,a]\)

(75.6)#\[u'(\xi) = \max \left\{ \beta \, \mathbb E_z \, \hat{R} \, (u' \circ \sigma)[\hat{R}(a - \xi) + \hat{Y}, \, \hat{Z}], \, u'(a) \right\}\]

\(K\) 背后的思想是,从定义可以看出,\(\sigma \in \mathscr C\) 满足欧拉方程当且仅当对于所有 \((a, z) \in \mathbf S\) 都有 \(K\sigma(a, z) = \sigma(a, z)\)

这意味着 \(K\)\(\mathscr C\) 中的不动点和最优消费政策完全重合(更多细节参见 [Ma et al., 2020])。

75.3.2. 收敛性质#

如前所述,我们在 \(\mathscr C\) 上配以如下度量

\[ \rho(c,d) := \sup_{(a,z) \in \mathbf S} \left| \left(u' \circ c \right)(a,z) - \left(u' \circ d \right)(a,z) \right|, \]

可以证明

  1. \((\mathscr C, \rho)\) 是一个完备度量空间,

  2. 存在一个整数 \(n\) 使得 \(K^n\)\((\mathscr C, \rho)\) 上的压缩映射,且

  3. \(K\)\(\mathscr C\) 中的唯一不动点是 \(\mathscr C\) 中的唯一最优政策。

现在,我们有了一个清晰的路径来成功地逼近最优政策:选择某个 \(\sigma \in \mathscr C\) 然后用 \(K\) 迭代直到收敛(用距离 \(\rho\) 衡量)。

75.3.3. 使用内生网格#

在研究该模型时,我们发现可以通过 内生网格方法 进一步加速时间迭代。

我们将在这里使用相同的方法。

该方法与最优增长模型的方法相同,只是需要记住消费并不总是内部的。

特别是,当资产水平较低时,最优消费可能等于资产。

75.3.3.1. 寻找最优消费#

内生网格方法(EGM)要求我们取一个储蓄值网格 \(s_i\),其中每个这样的 \(s\) 被解释为 \(s = a - c\)

对于最低的网格点,我们取 \(s_0 = 0\)

对于相应的 \(a_0, c_0\) 对,我们有 \(a_0 = c_0\)

这发生在接近原点的地方,资产较低,家庭消费其所能消费的一切。

虽然有许多解,但我们取 \(a_0 = c_0 = 0\),这固定了原点处的政策,有助于插值。

对于 \(s > 0\),根据定义,我们有 \(c < a\),因此消费是内部的。

因此 (75.5) 的最大值部分消失,我们在每个 \(s_i\) 处求解

(75.7)#\[c_i = (u')^{-1} \left\{ \beta \, \mathbb E_z \hat R (u' \circ \sigma) \, [\hat R s_i + \hat Y, \, \hat Z] \right\}\]

75.3.3.2. 迭代#

一旦我们得到 \(\{s_i, c_i\}\) 对,内生资产网格通过 \(a_i = c_i + s_i\) 获得。

另外,在上面的讨论中我们固定了 \(z \in \mathsf Z\),所以可以将其与 \(a_i\) 配对。

通过在每个 \(z\) 上对 \(\{a_i, c_i\}\) 插值,就可以得到政策 \((a,z) \mapsto \sigma(a,z)\) 的近似。

在下面的内容中,我们使用线性插值。

75.4. 实现#

以下是以 NamedTuple 表示的模型。

class IFP(NamedTuple):
    """
    一个 NamedTuple,使用 JAX 存储收入波动问题的基本参数。
    """
    γ: float
    β: float
    P: jnp.ndarray
    a_r: float
    b_r: float
    a_y: float
    b_y: float
    s_grid: jnp.ndarray
    η_draws: jnp.ndarray
    ζ_draws: jnp.ndarray


def create_ifp(
        γ=1.5,                      # 效用参数
        β=0.96,                     # 折现因子
        P=jnp.array([(0.9, 0.1),    # Z 的默认马尔可夫链
                    (0.1, 0.9)]),
        a_r=0.16,                   # R 冲击中的波动率项
        b_r=0.0,                    # R 冲击的均值偏移
        a_y=0.2,                    # Y 冲击中的波动率项
        b_y=0.5,                    # Y 冲击的均值偏移
        shock_draw_size=100,        # 用于蒙特卡洛
        grid_max=100,               # 外生网格最大值
        grid_size=100,              # 外生网格大小
        seed=1234                   # 随机种子
    ):
    """
    使用给定参数创建一个 IFP 实例。

    """
    # 假设 {R_t} 独立同分布且 ln R ~ N(b_r, a_r),检验稳定性
    ER = np.exp(b_r + a_r**2 / 2)
    assert β * ER < 1, "稳定性条件不成立。"

    # 使用 JAX 生成随机抽取
    key = jax.random.key(seed)
    subkey1, subkey2 = jax.random.split(key)
    η_draws = jax.random.normal(subkey1, (shock_draw_size,))
    ζ_draws = jax.random.normal(subkey2, (shock_draw_size,))
    s_grid = jnp.linspace(0, grid_max, grid_size)

    return IFP(
        γ, β, P, a_r, b_r, a_y, b_y, s_grid, η_draws, ζ_draws
    )


def u_prime(c, γ):
    """边际效用"""
    return c**(-γ)

def u_prime_inv(c, γ):
    """边际效用的逆函数"""
    return c**(-1/γ)

def R(z, ζ, a_r, b_r):
    """资产的总收益率"""
    return jnp.exp(a_r * ζ + b_r)

def Y(z, η, a_y, b_y):
    """劳动收入"""
    return jnp.exp(a_y * η + (z * b_y))

这是使用 JAX 的 Coleman-Reffett 算子:

def K(
        c_in: jnp.array,   # c_in[i, z] = a_in[i, z] 处的消费
        a_in: jnp.array,   # a_in[i, z] 是资产网格
        ifp: IFP
    ):
    """
    使用 JAX 结合内生网格方法的收入波动问题的
    Coleman--Reffett 算子。

    """

    # 从 ifp 中提取参数
    γ, β, P, a_r, b_r, a_y, b_y, s_grid, η_draws, ζ_draws = ifp
    n = len(P)

    def compute_expectation(s, z):
        def inner_expectation(z_hat):
            def compute_term(η, ζ):
                R_hat = R(z_hat, ζ, a_r, b_r)
                Y_hat = Y(z_hat, η, a_y, b_y)
                a_val = R_hat * s + Y_hat
                # 对消费进行插值
                c_interp = jnp.interp(a_val, a_in[:, z_hat], c_in[:, z_hat])
                mu = u_prime(c_interp, γ)
                return R_hat * mu
            # 对所有冲击组合进行向量化
            η_grid, ζ_grid = jnp.meshgrid(η_draws, ζ_draws, indexing='ij')
            terms = vmap(vmap(compute_term))(η_grid, ζ_grid)
            return P[z, z_hat] * jnp.mean(terms)
        # 对 z_hat 状态求和
        Ez = jnp.sum(vmap(inner_expectation)(jnp.arange(n)))
        return u_prime_inv(β * Ez, γ)

    # 对 s_grid 和 z 进行向量化
    compute_exp_v1 = vmap(compute_expectation, in_axes=(None, 0))
    compute_exp_v2 = vmap(compute_exp_v1,      in_axes=(0, None))
    c_out = compute_exp_v2(s_grid, jnp.arange(n))
    # 计算内生资产网格
    a_out = s_grid[:, None] + c_out
    # 在 (0, 0) 处固定消费-资产对 
    c_out = c_out.at[0, :].set(0)
    a_out = a_out.at[0, :].set(0)

    return c_out, a_out

下一个函数使用 JAX 通过时间迭代求解最优消费政策的近似:

@jax.jit
def solve_model(
        ifp: IFP,
        c_init: jnp.ndarray,  # 内生网格上 σ 的初始猜测
        a_init: jnp.ndarray,  # 初始内生网格
        tol: float = 1e-5,
        max_iter: int = 1000
    ) -> jnp.ndarray:
    " 使用 EGM 的时间迭代求解模型。 "

    def condition(loop_state):
        c_in, a_in, i, error = loop_state
        return (error > tol) & (i < max_iter)

    def body(loop_state):
        c_in, a_in, i, error = loop_state
        c_out, a_out = K(c_in, a_in, ifp)
        error = jnp.max(jnp.abs(c_out - c_in))
        i += 1
        return c_out, a_out, i, error

    i, error = 0, tol + 1
    initial_state = (c_init, a_init, i, error)
    final_loop_state = jax.lax.while_loop(condition, body, initial_state)
    c_out, a_out, i, error = final_loop_state

    return c_out, a_out

现在我们可以创建一个实例并使用 JAX 求解模型:

ifp = create_ifp()

设置初始条件:

# 初始猜测 σ = 消费所有资产
k = len(ifp.s_grid)
n = len(ifp.P)
σ_init = jnp.empty((k, n))
for z in range(n):
    σ_init = σ_init.at[:, z].set(ifp.s_grid)
a_init = σ_init.copy()

让我们用 JAX 生成一个近似解:

σ_star, a_star = solve_model(ifp, σ_init, a_init)

让我们再用计时器试一次。

with qe.Timer(precision=8):
    σ_star, a_star = solve_model(ifp, σ_init, a_init)
    σ_star.block_until_ready()
10.43059778 seconds elapsed

75.5. 模拟#

让我们回到默认模型,研究资产的平稳分布。

我们的计划是让大量家庭向前推进 \(T\) 期,然后绘制资产横截面分布的直方图。

设置 num_households=50_000, T=500

首先我们编写一个函数,将单个家庭向前模拟,并记录资产的最终值。

该函数接受一对解 c_veca_vec,将其理解为与给定模型 ifp 相关联的最优政策。

def simulate_household(
        key, a_0, z_idx_0, c_vec, a_vec, ifp, T
    ):
    """
    模拟单个家庭 T 期,以逼近资产的平稳分布。

    - key 是随机数生成器的状态
    - ifp 是 IFP 的一个实例
    - c_vec, a_vec 是 ifp 的最优消费政策和内生网格

    """
    # 从 ifp 中提取参数
    γ, β, P, a_r, b_r, a_y, b_y, s_grid, η_draws, ζ_draws = ifp
    n_z = len(P)

    # 为消费政策创建插值函数
    σ = lambda a, z_idx: jnp.interp(a, a_vec[:, z_idx], c_vec[:, z_idx])

    # 向前模拟 T 期
    def update(t, state):
        a, z_idx = state
        # 从 P[z, z'] 中抽取下一期冲击 z'
        current_key = jax.random.fold_in(key, 3*t)
        z_next_idx = jax.random.choice(current_key, n_z, p=P[z_idx]).astype(jnp.int32)
        # 为收入抽取 η 冲击
        η_key = jax.random.fold_in(key, 3*t + 1)
        η = jax.random.normal(η_key)
        # 为收益率抽取 ζ 冲击
        ζ_key = jax.random.fold_in(key, 3*t + 2)
        ζ = jax.random.normal(ζ_key)
        # 计算随机收益率
        R_next = R(z_next_idx, ζ, a_r, b_r)
        # 计算收入
        Y_next = Y(z_next_idx, η, a_y, b_y)
        # 更新资产:a' = R' * (a - c) + Y'
        a_next = R_next * (a - σ(a, z_idx)) + Y_next
        # 返回更新后的状态
        return a_next, z_next_idx

    initial_state = a_0, z_idx_0
    final_state = jax.lax.fori_loop(0, T, update, initial_state)
    a_final, _ = final_state
    return a_final

现在我们编写一个函数,并行模拟许多家庭。

@partial(jax.jit, static_argnums=(3, 4, 5))
def compute_asset_stationary(
        c_vec, a_vec, ifp, num_households=50_000, T=500, seed=1234
    ):
    """
    模拟 num_households 个家庭 T 期,以逼近资产的平稳分布。

    返回资产持有量的最终横截面。

    - ifp 是 IFP 的一个实例
    - c_vec, a_vec 是最优消费政策和内生网格。

    """
    # 从 ifp 中提取参数
    γ, β, P, a_r, b_r, a_y, b_y, s_grid, η_draws, ζ_draws = ifp

    # 从 资产 = 储蓄网格最大值 / 2 开始
    a_0_vector = jnp.full(num_households, s_grid[-1] / 2)
    # 初始化每个家庭的外生状态
    z_idx_0_vector = jnp.zeros(num_households).astype(jnp.int32)

    # 对许多家庭进行向量化
    key = jax.random.key(seed)
    keys = jax.random.split(key, num_households)
    # 在 (key, a_0, z_idx_0) 上向量化 simulate_household
    sim_all_households = jax.vmap(
        simulate_household, in_axes=(0, 0, 0, None, None, None, None)
    )
    assets = sim_all_households(keys, a_0_vector, z_idx_0_vector, c_vec, a_vec, ifp, T)

    return jnp.array(assets)

我们需要一些不平等度量来进行可视化,所以让我们先定义它们:

def gini_coefficient(x):
    """
    计算数组 x 的基尼系数。

    """
    x = jnp.asarray(x)
    n = len(x)
    x_sorted = jnp.sort(x)
    # 计算基尼系数
    cumsum = jnp.cumsum(x_sorted)
    a = (2 * jnp.sum((jnp.arange(1, n+1)) * x_sorted)) / (n * cumsum[-1])
    return a - (n + 1) / n


def top_share(
        x: jnp.array,   # 财富值数组
        p: float=0.01   # 头部家庭的比例(默认 0.01 表示前 1%)
    ):
    """
    计算前 p 比例家庭所持有的总财富份额。

    """
    x = jnp.asarray(x)
    x_sorted = jnp.sort(x)
    # 前 p% 中的家庭数量
    n_top = int(jnp.ceil(len(x) * p))
    # 前 p% 持有的财富
    wealth_top = jnp.sum(x_sorted[-n_top:])
    # 总财富
    wealth_total = jnp.sum(x_sorted)
    return wealth_top / wealth_total

现在我们调用该函数,生成资产分布并将其可视化:

ifp = create_ifp()
# 提取用于初始化的参数
s_grid = ifp.s_grid
n_z = len(ifp.P)
a_init = s_grid[:, None] * jnp.ones(n_z)
c_init = a_init
c_vec, a_vec = solve_model(ifp, c_init, a_init)
assets = compute_asset_stationary(c_vec, a_vec, ifp, num_households=200_000)

# 为图形计算基尼系数
gini_plot = gini_coefficient(assets)

# 绘制对数财富直方图
fig, ax = plt.subplots(figsize=(10, 6))
ax.hist(jnp.log(assets), bins=40, alpha=0.5, density=True)
ax.set(xlabel='对数资产', ylabel='密度', title="财富分布")
plt.tight_layout()
plt.show()
findfont: Failed to find font weight normal, now using 600.
findfont: Failed to find font weight normal, now using 600.
_images/86ef72fa55e49918bd0bdda74a03b3c56302edd91afd29ca931f45fb677802d0.png

直方图显示了对数财富的分布。

请记住我们看的是对数值,直方图表明分布有较长的右尾。

下面我们更详细地研究这一点。

75.6. 财富不平等#

让我们通过计算这一现象的一些标准度量来考察财富不平等。

我们还将考察不平等程度如何随利率变化。

75.6.1. 度量不平等#

让我们打印出模拟结果中的基尼系数和前 1% 财富份额:

gini = gini_coefficient(assets)
top1 = top_share(assets, p=0.01)

print(f"基尼系数:{gini:.4f}")
print(f"前 1% 财富份额:{top1:.4f}")
基尼系数:0.7847
前 1% 财富份额:0.7306

最近的数据表明

  • 美国财富的基尼系数约为 0.8

  • 前 1% 的财富份额超过 0.3

我们具有随机收益的模型生成的基尼系数接近经验值,这表明资本收入风险是财富不平等的一个重要因素。

然而,前 1% 的财富份额过大。

我们的模型需要适当的校准和进一步的工作——我们暂时搁置这些任务。

75.7. 练习#

练习 75.1

绘制基尼系数如何随资产收益的波动性变化。

具体而言,计算 a_r 从 0.10 到 0.16 变化时的基尼系数(至少使用 5 个不同的值),并绘制结果图。

这告诉我们资本收入风险与财富不平等之间的关系是什么?

练习 75.2

绘制基尼系数如何随劳动收入的波动性变化。

具体而言,计算 a_y 从 0.125 到 0.20 变化时的基尼系数,并绘制结果图。在本练习中设置 a_r=0.10

这告诉我们劳动收入风险与财富不平等之间的关系是什么?通过改变劳动收入波动性,我们能否达到与改变收益波动性同样程度的不平等上升?