137. 失业的线性模型#

GPU

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

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

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

  2. 选择 Colab

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

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

我们首先安装 numpyrojax

!pip install numpyro jax

Hide code cell output

Requirement already satisfied: numpyro in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (0.21.0)
Requirement already satisfied: jax in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (0.11.1)
Requirement already satisfied: jaxlib>=0.7.0 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from numpyro) (0.11.1)
Requirement already satisfied: multipledispatch in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from numpyro) (1.0.0)
Requirement already satisfied: numpy in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from numpyro) (2.4.6)
Requirement already satisfied: tqdm in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from numpyro) (4.68.2)
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: scipy>=1.15 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from jax) (1.18.0)

我们还要安装 pandas_datareader,用它从 FRED 下载数据:

!pip install pandas_datareader

Hide code cell output

Requirement already satisfied: pandas_datareader in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (0.11.1)
Requirement already satisfied: lxml in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from pandas_datareader) (6.1.2)
Requirement already satisfied: pandas>=2.1.4 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from pandas_datareader) (3.0.3)
Requirement already satisfied: requests>=2.19.0 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from pandas_datareader) (2.34.2)
Requirement already satisfied: setuptools in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from pandas_datareader) (83.0.0)
Requirement already satisfied: numpy>=1.26.0 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from pandas>=2.1.4->pandas_datareader) (2.4.6)
Requirement already satisfied: python-dateutil>=2.8.2 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from pandas>=2.1.4->pandas_datareader) (2.9.0.post0)
Requirement already satisfied: six>=1.5 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from python-dateutil>=2.8.2->pandas>=2.1.4->pandas_datareader) (1.17.0)
Requirement already satisfied: charset_normalizer<4,>=2 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from requests>=2.19.0->pandas_datareader) (3.4.7)
Requirement already satisfied: idna<4,>=2.5 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from requests>=2.19.0->pandas_datareader) (3.18)
Requirement already satisfied: urllib3<3,>=1.26 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from requests>=2.19.0->pandas_datareader) (2.7.0)
Requirement already satisfied: certifi>=2023.5.7 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from requests>=2.19.0->pandas_datareader) (2026.6.17)

137.1. 概述#

本讲座将贝叶斯估计应用于美国失业率的线性 AR(1)过程模型。

我们在 AR(1)参数的后验分布 中已经接触过相关机制,但那里的数据是模拟的,重点也偏理论。

在这里,我们将仔细研究真实数据,围绕一个在 20 世纪 80 年代和 90 年代分裂了宏观经济学家的问题来组织本讲座:失业是否是一个随机游走

在这个过程中,我们会观察到一种模型无法捕捉的非对称性。

这将引出题为 具有非对称冲击的失业动态 的后续讲座。

AR(1)参数的后验分布非共轭先验 一样,我们通过 NumPyro 中的 NUTS 采样器对后验进行采样来完成估计。

(关于其工作原理的简要介绍,参见 非共轭先验 中的 NUTS 介绍。)

让我们从一些导入开始。

import numpy as np
import matplotlib.pyplot as plt
import datetime as dt
from pandas_datareader import data as web

import jax.numpy as jnp
from jax import random
import numpyro
import numpyro.distributions as dist
from numpyro.infer import MCMC, NUTS
import matplotlib as mpl  # i18n
FONTPATH = "fonts/SourceHanSerifSC-SemiBold.otf"  # i18n
mpl.font_manager.fontManager.addfont(FONTPATH)  # i18n
mpl.rcParams['font.family'] = ['Source Han Serif SC']  # i18n

137.2. 自然率与滞后效应#

在 20 世纪 80 年代初,Nelson and Plosser [1982] 开启了单位根文献。

他们检验了 14 个宏观经济时间序列,发现只有其中一个能够拒绝随机游走。

粗略地说,这意味着对于大多数宏观经济序列而言,冲击的影响看起来是永久的,而不是暂时的。

对于失业而言,这一观点体现在 [Blanchard and Summers, 1986] 的**滞后效应假说(hysteresis hypothesis)**中。

该假说认为,对失业的冲击可能或多或少是永久性的,因为失业期会侵蚀技能以及对劳动力市场的依附。

相反,[Friedman, 1968] 的**自然率假说(natural rate hypothesis)**认为,失业率围绕一个稳定的均衡率波动——因此冲击是暂时的,而非永久的。

这场辩论之所以重要,是因为这两种观点意味着不同的政策。

具体来说,如果冲击是永久性的,那么一场严重的衰退会留下持久的创伤,从而促使人们采取补救行动。

在这里,我们用贝叶斯估计重新审视这个问题。

备注

上述历史中有一个讽刺之处。

虽然 Nelson and Plosser [1982] 点燃了单位根之争,从而引出了滞后效应假说,但他们所拒绝了单位根的那个宏观经济序列 恰恰就是失业率。

137.3. 数据#

我们使用美国民用失业率,即来自 FRED 的 UNRATE 序列,月度且经过季节性调整。

start, end = dt.datetime(1948, 1, 1), dt.datetime(2024, 12, 31)
unrate = web.DataReader("UNRATE", "fred", start, end)["UNRATE"]

2020 年 COVID-19 引起的峰值是一个极端异常值,由我们的模型一无所知的事件驱动,所以我们把它剔除。

pre_covid = unrate[unrate.index < "2020-01-01"]
u_monthly = pre_covid.to_numpy()
print(f"{len(u_monthly)} monthly observations")
864 monthly observations

这是月度序列。

fig, ax = plt.subplots()
ax.plot(pre_covid.index, u_monthly, lw=2)
ax.set_xlabel('年份')
ax.set_ylabel('失业率 (%)')
plt.show()
findfont: Failed to find font weight normal, now using 600.
_images/58a61f80bbcff874e62b20ba40231ce0428653b0140e12c960db67511fb014b8.png

图 137.1 美国月度失业率,1948–2019#

图 137.1 所示,失业率在衰退中急剧上升,在复苏中缓慢下降,但它始终保持在一个区间内——在整个战后时期大致为 3% 到 11%。

(请记住这个区间,因为它与单位根之争相关联,我们将在下文讨论。)

137.4. 失业的线性模型#

现在让我们使用月度数据来设置并估计模型。

137.4.1. 模型#

我们设失业率被拉回到一个正常水平 \(\bar u\)

(137.1)#\[ u_{t+1} = \bar u + \phi\,(u_t - \bar u) + \varepsilon_{t+1}, \qquad \varepsilon_{t+1} \sim N(0, \sigma^2), \]

其中 \(0 \le \phi < 1\)

这是一个线性 AR(1)过程模型,写成这种形式使得 \(\bar u\) 是序列所回归的水平,而 \(\phi\) 度量持续性。

\(\phi\) 越接近 1,序列在冲击后返回 \(\bar u\) 的速度就越慢;随机游走是极限情形 \(\phi = 1\)

137.4.2. 先验#

我们把 \(\bar u\)\(\phi\)\(\sigma\) 视为未知,并对它们施加弱信息先验。

我们给 \(\phi\) 一个 \([0, 1)\) 上的均匀先验。

上端点被有意排除,因为这样做可以将我们限制在平稳区域内。

这是必要的,我们将在下文看到原因。

同时,这个先验仍然允许 \(\phi\) 在数据要求下尽可能地接近 1。

我们将 \(\bar u\) 以一个合理的自然率为中心,并配以相当宽的正态先验,并给冲击尺度 \(\sigma\) 一个半正态先验。

我们把模型写成一个 NumPyro 函数:每个 numpyro.sample 引入一个随机变量,关键字 obs= 将最后一个变量与数据绑定,从而提供似然。

def linear_model(u):
    ubar = numpyro.sample("ubar",  dist.Normal(5.5, 2.0))      # 自然率先验
    φ    = numpyro.sample("phi",   dist.Uniform(0.0, 1.0))     # 持续性先验
    σ    = numpyro.sample("sigma", dist.HalfNormal(1.0))       # 波动率先验
    μ = ubar + φ * (u[:-1] - ubar)
    numpyro.sample("u_obs", dist.Normal(μ, σ), obs=u[1:])

向量 μ 保存了条件均值 \(\bar u + \phi(u_t - \bar u)\),而 obs=u[1:] 表示每个下一个值都是从 \(N(\mu_t, \sigma^2)\) 中抽取的。

这一条语句就编码了整个似然。(关于编写 NumPyro 模型的更多内容,参见 非共轭先验。)

137.4.3. 估计#

我们用 NUTS 对后验进行采样,运行四条链以便检查收敛性。

我们使用 chain_method="vectorized",它在单个设备上同时对所有链进行评估,因此相同的代码在 CPU 或 GPU 上都能不变地运行。

def run_nuts(model, data, seed=0, num_warmup=1000, num_samples=2000, num_chains=4):
    "使用 NUTS 采样器对 NumPyro 模型进行采样。"
    mcmc = MCMC(NUTS(model),
                num_warmup=num_warmup, num_samples=num_samples,
                num_chains=num_chains, chain_method="vectorized",
                progress_bar=False)
    mcmc.run(random.key(seed), jnp.asarray(data))
    return mcmc

我们将模型拟合到月度数据。

mcmc_monthly = run_nuts(linear_model, u_monthly)

现在我们检查输出。

mcmc_monthly.print_summary()
                mean       std    median      5.0%     95.0%     n_eff     r_hat
       phi      0.99      0.00      0.99      0.99      1.00   2904.53      1.00
     sigma      0.21      0.00      0.21      0.20      0.22   4583.96      1.00
      ubar      5.69      1.14      5.71      3.72      7.42   3645.82      1.00

Number of divergences: 0

每一行总结了一个参数的后验。

meanmedian 以及 5.0%/95.0% 列给出后验均值、中位数和 90% 可信区间;std 是后验标准差。

最后两列是收敛诊断:n_eff 是有效独立抽样数,r_hat 比较链内和链间的变化——一个非常接近 \(1.0\) 的值意味着各链一致,采样器已经收敛。

这里 r_hat 基本上为 1,n_eff 也很大,所以我们可以信任这些抽样。

对我们而言重要的数值是 \(\phi\) 的后验:它的质量紧紧地贴向 1。

具体来说,均值和中位数都非常接近 1,而标准差非常小。

换句话说,在月度频率上,美国失业率几乎是一个随机游走。

这就是滞后效应的边界。

因此,我们发现自然率观点和滞后效应观点在月度数据中几乎无法区分——估计结果与两者都一致。

这就是为什么那个时代的单位根检验难以平息这场辩论 [Røed, 1997]

137.5. 随机游走会漂离#

虽然上面的估计似乎为单位根假说提供了合理的支持,但有一个充分的理由认为它是错误的。

要看到这个论证,假设失业率真的是一个纯粹的随机游走,其中 \(\phi = 1\)

\[ u_{t+1} = u_t + \varepsilon_{t+1}, \qquad \varepsilon_{t+1} \sim N(0, \sigma^2). \]

那么 \(u_t = u_0 + \sum_{s=1}^t \varepsilon_s\),因此其方差无界增长:\(\operatorname{Var}(u_t) = t\sigma^2\)

分布会永远扩散,最终概率质量会离开每一个有界区间。

我们可以通过模拟许多随机游走路径来观察这一点,用观测到的单月变化来设定冲击大小,并观察它们的扩散。

rng = np.random.default_rng(0)
T = len(u_monthly)
σ_rw = np.diff(u_monthly).std()
paths = u_monthly[0] + np.cumsum(rng.normal(0, σ_rw, size=(400, T)), axis=1)

u_min, u_max = u_monthly.min(), u_monthly.max()

fig, ax = plt.subplots()
ax.plot(paths[:60].T, color='C0', lw=0.5, alpha=0.3)
ax.axhspan(u_min, u_max, color='C1', alpha=0.15, label='观测范围')
ax.axhline(u_min, color='C1', ls='--', lw=1.5)
ax.axhline(u_max, color='C1', ls='--', lw=1.5)
ax.set_xlabel('自 1948 年起的月数')
ax.set_ylabel('失业率 (%)')
ax.legend()
plt.show()
_images/1e7bef216d394603c7b00156fc0fd4b418b58d5fb08e3030f75569677aa9045b.png

图 137.2 随机游走离开观测范围#

图 137.2 中,虚线标记了数据中曾出现过的最低和最高失业率,阴影区域是它们之间的区间。

模拟路径像 \(\sqrt{t}\) 一样扩散,并迅速蔓延到这个区间之外,甚至包括负的失业率。

随机游走没有锚点,但失业率显然有——它在七十年里一直保持在一个狭窄的区间内。

所以我们已经可以排除精确的随机游走。

当我们检查年度数据时,这一点会更加清楚。

137.6. 月度与年度#

月度数据把 \(\phi\) 钉在了 1 上。

不同的频率能否让我们看到更多东西?

我们用年末值构造一个年度序列,并对它拟合相同的模型。

u_annual = pre_covid.resample("YE").last().to_numpy()
print(f"{len(u_annual)} annual observations")
72 annual observations

我们对这个更短的序列拟合相同的模型。

mcmc_annual = run_nuts(linear_model, u_annual)

现在我们比较两个频率下 \(\phi\) 的后验。

φ_m = np.asarray(mcmc_monthly.get_samples()["phi"])
φ_a = np.asarray(mcmc_annual.get_samples()["phi"])

fig, ax = plt.subplots()
ax.hist(φ_m, bins=50, density=True, alpha=0.6, label='月度')
ax.hist(φ_a, bins=50, density=True, alpha=0.6, label='年度')
ax.set_xlabel('$\\phi$')
ax.legend()
plt.show()
findfont: Failed to find font weight normal, now using 600.
_images/9c071cb937764494e48b4203947f1312a6c6acf5ca9dc6e2d859dd6048a4b1da.png

图 137.3 持续性参数的后验#

图 137.3 说明了我们的主要发现:年度的 \(\phi\) 后验明显低于 1,具有清晰的回归,而月度的后验则紧贴边界。

这并不矛盾。

如果月度持续性是 \(\phi\),那么对于年末值而言,持续性大约是 \(\phi^{12}\),而将一个接近 1 的数字提升到十二次方会把它明显拉到 1 以下——这与我们的年度估计一致。

137.7. 模型遗漏了什么#

再次审视 图 137.1,我们注意到失业率在衰退中迅速跳升,在复苏中缓慢下降。

这是一种我们当前模型无法复制的非对称性

要弄清楚原因,我们仔细审视模型中唯一随机的部分——冲击。

我们分三步进行:陈述模型对冲击的假设,从数据中恢复冲击,并比较两者。

137.7.1. 模型对冲击的假设#

给定上一期的失业率 \(u_t\) 和参数,下一期的失业率是一个确定性的条件均值加上一个冲击:

\[ u_{t+1} = \underbrace{\bar u + \phi\,(u_t - \bar u)}_{\text{条件均值}} + \varepsilon_{t+1}, \qquad \varepsilon_{t+1} \sim N(0, \sigma^2). \]

重新整理,冲击就是实际发生的值与模型预期值之间的差距:

\[ \varepsilon_{t+1} = u_{t+1} - \big(\bar u + \phi\,(u_t - \bar u)\big). \]

模型对这些冲击提出了一个强烈且可检验的断言:它们是从一个对称的正态分布中独立抽取的。

如果这个断言成立,我们从数据中恢复的冲击应该看起来像一条钟形曲线。

如果它失败,它失败的方式将告诉我们模型遗漏了什么。

137.7.2. 恢复冲击#

我们无法直接读出冲击,因为我们不知道参数 \(\bar u\)\(\phi\)

所以我们对它们进行估计,为每个参数代入一个单一的代表性值。

我们使用后验中位数——对于快速诊断而言这是一个合理的选择。

med = {k: np.median(np.asarray(mcmc_monthly.get_samples()[k]))
       for k in ("ubar", "phi", "sigma")}
resid = u_monthly[1:] - (med["ubar"] + med["phi"] * (u_monthly[:-1] - med["ubar"]))

resid 数组保存了我们估计的冲击,每一个对应一个月度到月度的转移——即模型的残差

切片就是把每个月与它前面的那个月对齐。

resid 的每个元素是 \(u_{t+1} - \big(\hat{\bar u} + \hat\phi\,(u_t - \hat{\bar u})\big)\),即模型的向前一步预测误差,其中带帽子的符号表示中位数估计。

(因为我们的估计 \(\hat\phi\) 非常接近 1,所以这几乎就是月度变化 \(u_{t+1} - u_t\)。)

137.7.3. 与高斯分布比较#

现在我们来问,这些残差看起来是否是高斯的。

我们叠加一个正态密度,其标准差被设置为等于残差本身的标准差。

这是有意为之的:残差的均值已经接近零,所以匹配方差就使得均值和扩散都一致。

剩下的任何差异就是形状上的差异——这正是我们想要分离出来的。

度量形状的一种方式是偏度(skewness),即三阶标准化矩:

\[ \text{skew} = \frac{\frac1n \sum_i (\varepsilon_i - \bar\varepsilon)^3}{\Big(\frac1n \sum_i (\varepsilon_i - \bar\varepsilon)^2\Big)^{3/2}}. \]

对于任何对称分布,这个度量为零;当右尾更长时,它为正。

现在我们绘制残差并计算它们的偏度:

def skewness(x):
    x = x - x.mean()
    return (x**3).mean() / x.std()**3

fig, ax = plt.subplots()
ax.hist(resid, bins=60, density=True, alpha=0.6, label='残差')
grid = np.linspace(resid.min(), resid.max(), 200)
gauss = np.exp(-grid**2 / (2 * resid.std()**2)) / (resid.std() * np.sqrt(2 * np.pi))
ax.plot(grid, gauss, 'C1', lw=2, label='对称高斯')
ax.set_xlabel('模型无法解释的单月变化')
ax.legend()
plt.show()

print(f"residual skewness = {skewness(resid):.2f}")
_images/b41be67f638d3dd66d32bdc60a397d44936a290858b7789ec427e4bc1880389c.png

图 137.4 模型残差呈右偏#

residual skewness = 0.39

图 137.4 中的残差在两个方面偏离了高斯分布。

它们是重尾的尖峰的:比钟形曲线所允许的有更多微小变化,也有更多大幅变化。

此外,它们是右偏的,最大的意外出现在上行方向(衰退)。

对称高斯分布(橙色)无法匹配这两个特征:它把向上和向下的冲击视为等可能,也无法容纳偶尔出现的非常大的跳跃。

所以我们的模型注定会误读数据,把罕见的向上跳跃和长期温和的下滑视为同一种冲击。

137.7.4. 关于代入法的说明#

一个贝叶斯纯粹主义者会反对说,这里并不存在单一的残差序列。

\((\bar u, \phi)\) 的每一次后验抽样都意味着它自己的残差序列,而我们只是选择了中位数。

这个反对是合理的,而这个检验的完全贝叶斯版本——从后验中模拟整个数据集并比较某个汇总统计量——正是我们在 具有非对称冲击的失业动态 中所做的。

这个代入法检验是那个更完整测试的一个快速预览。

捕捉这种非对称性是下一讲座的任务,在那里我们对冲击本身而非回归曲线进行建模。

137.8. 练习#

讲座 预测 AR(1) 过程 通过模拟未来路径来预测一个 AR(1)过程,既考虑一个固定参数值为条件的预测分布,也考虑一个参数后验不确定性进行积分的预测分布。

以下练习将这些思想应用到我们拟合的失业模型上。

练习 137.1

使用拟合的年度模型,从最后观测到的值开始,用两种方式预测未来 \(H = 15\) 年的失业率:

  1. 代入法:将参数固定在其后验中位数上,并模拟许多未来路径;

  2. 扩展法:对于每条未来路径,从后验中抽取一组新的 \((\bar u, \phi, \sigma)\)

在同一坐标轴上绘制各自的 90% 预测区间并进行比较。

哪个区间更宽,为什么?

练习 137.2

沿用 Wecker 的方法(参见 预测 AR(1) 过程),我们也可以为一个路径统计量——整条未来路径的非线性函数——构造一个预测分布。

未来 \(H = 8\) 年内的最高失业率作为统计量:一个衡量未来几年可能有多糟糕的简单指标。

使用扩展模拟(每条路径抽取一组后验样本),为每条路径计算这个最大值,并绘制它的预测分布。

在未来八年内的某个时点失业率达到至少 \(7\%\)——即衰退区间——的后验预测概率是多少?