动态结构模型:DDCM、Rust 与 Hotz-Miller
当今天的选择影响明天的状态时,静态 logit 不再适用。Rust (1987) 的 bus engine replacement 模型、NFXP 算法、CCP 与 Hotz-Miller 两步估计,是动态离散选择的基石。
01 为什么需要动态模型:静态选择的局限
前几页讲的离散选择模型(logit、BLP)都是静态的:消费者在每个时期独立地最大化当期效用,今天的选择不影响明天的状态。但现实中大量决策是动态的:
- 换不换公交车发动机?换了要花一笔钱,但发动机变新;不换,下个月可能坏在路上。
- 要不要生孩子?今天生孩子会影响未来 20 年的劳动供给与消费。
- 是否参与社保 / 退休?今天的退休决定了未来一辈子的收入流。
- 企业何时进入/退出市场?进入是沉没成本,但决定了未来的利润流。
- 研发投入 / 技术升级?今天的投入影响未来的生产率。
在这些问题里,当前决策改变了未来的状态分布,决策者必须比较"今天的效用 + 贴现后的未来价值"。这就是动态离散选择模型(Dynamic Discrete Choice Model, DDCM)。
静态 logit 里,选择概率只取决于当期效用差;DDCM 里,选择概率取决于"当期效用 + 未来价值函数"之差。价值函数本身又是选择概率和状态转移的不动点——这就把"估计"变成了"嵌套不动点问题"。
02 动态离散选择框架(DDCM)
2.1 基本要素
一个标准的 DDCM 由以下要素构成:
- 决策者(agent):在每个离散时期 $t = 1,2,\dots$ 做选择;
- 状态变量 $x_t \in \mathcal{X}$:随时间演化的状态(如 bus 的里程、家庭的孩子数);
- 选择集 $d_t \in \mathcal{D}(x_t)$:从状态 $x_t$ 出发可做的离散动作;
- 当期效用 $u(x_t, d_t)$:选择 $d_t$ 时的 flow utility;
- 转移概率 $p(x_{t+1} | x_t, d_t)$:选择 $d_t$ 后状态如何演化;
- taste shock $\varepsilon_{t,d}$:每个选择的 i.i.d. 极端价值偏好冲击(和 logit 一样);
- 贴现因子 $\beta \in (0,1)$:未来效用的贴现率。
2.2 决策者的目标函数
决策者在每一期选择 $d_t$ 来最大化贴现后的期望效用:
由于问题是马尔可夫的(转移只依赖当期状态和选择),最优决策可以用 Bellman 方程递归刻画。
2.3 Rust (1987) 的识别假设:条件独立与充分性
Rust 证明 DDCM 可被部分识别依赖两个关键假设,这是 NFXP 可估计的前提,必须明确写出:
变量定义:$g(\cdot)$ 为 Type-I 极值密度。识别条件:(CI) 下未来 taste shock 与状态、选择独立,故可对 $\varepsilon'$ 解析积分(log-sum-exp);(S) 充分性假设 $x_t$ 总结了所有状态信息。二者缺一,Bellman 不可降维、CCP 无 logit 形式。经济直觉:CI 是"今天的选择不影响明天 taste shock 的分布",这是把动态问题压成静态 logit 递归的关键。
03 Bellman 方程与条件选择概率(CCP)
3.1 Bellman 方程
定义 $V(x, \varepsilon)$ 为在状态 $(x, \varepsilon)$ 下的期望最优值函数(ex ante value,即在知道 $\varepsilon$ 之前的价值):
展开来看,方括号里三项分别是:(1) 当期 flow utility $u(x,d)$;(2) 当期 taste shock $\varepsilon_d$;(3) 贴现后的未来期望价值。注意未来价值里的期望是对两个随机性取的:状态转移 $x' | x,d$ 与下一期 taste shock $\varepsilon'$。
3.2 Choice-specific value(不含 $\varepsilon$)
定义选择 $d$ 的"选择特定价值"(exclusive of $\varepsilon$):
这是 Rust (1987) 的关键推导:由于 $\varepsilon_d$ 是 i.i.d. Type-I Extreme Value,对 $\varepsilon'$ 的积分有解析形式——log-sum-exp 表达式 $\log \sum_{d'} e^{v(x',d')}$。这把"对连续分布积分"简化成了一个封闭公式。
3.3 条件选择概率(CCP)
在状态 $x$ 下,决策者选择 $d$ 的概率(在 $\varepsilon$ 上积分后)是一个 logit 形式:
这是 DDCM 的核心对象:CCP 把 structural 参数 $\theta$ 和数据中可观察的选择频率联系起来。给定参数 $\theta$,我们能算出在每个状态下每个选择被选中的概率;反过来,给定数据,我们能通过最大似然或矩估计反推 $\theta$。
04 Rust 模型:Bus Engine Replacement
Rust (1987 Econometrica) 的经典例子是 Madison Metropolitan Bus (MMB) 发动机更换问题。
4.1 设定
- 状态 $x$:bus 的里程(mileage),离散化为一个分箱变量(例如 $x \in \{0,1,\dots,174\}$ 表示每 5000 英里一档);
- 选择 $d \in \{0, 1\}$:
- $d=0$:继续使用(maintain),本期支付运行成本 $c(x)$;
- $d=1$:更换发动机(replace),本期一次性支付成本 $RC$,里程重置为 0。
- 当期效用(成本前加负号):
- $u(x, 0) = -c(x) = -RC \cdot \theta_1 \cdot \log(1+x/\theta_2)$ (运行成本随里程上升);
- $u(x, 1) = -RC$ (换新发动机的固定成本,里程归 0)。
- 转移:选择 $d=0$ 时,里程按 Poisson 过程增加 0 或 1 档;选择 $d=1$ 时,里程重置为 0 并增加一期。
- 贴现因子 $\beta$:通常校准为 0.95 或 0.99。
4.2 估计目标
待估参数 $\theta = \{\theta_1, \theta_2\}$(运行成本函数的斜率和曲率),有时还包括 $RC$(重置成本)。Rust 用最大似然估计:对每辆 bus 在每期的选择 $(x_t, d_t)$,似然贡献就是 $P(d_t | x_t; \theta)$,整个似然函数是这些 CCP 的乘积。
变量定义:$v^*(x,d;\theta)$ 为给定 $\theta$ 时内层 Bellman 不动点解;$N$ 为 bus 数,$T_i$ 为第 $i$ 辆观测期数。识别条件:$\beta$ 不可与 flow utility 同时识别(Rust 1987 注明),须校准 $\beta$ 或加排除约束。经济直觉:这是"内层解 Bellman、外层优化似然"的嵌套目标,外层每次求梯度都要重新解内层不动点。
05 NFXP 算法:嵌套不动点
NFXP 的"内层解 Bellman、外层优化参数"是经典嵌套结构,容易慢、不收敛。想了解如何用 MPEC 把 Bellman 方程改写成等式约束、与参数一次性联合求解?先学 基础知识库·MPEC →。
Rust (1987) 提出的 NFXP (Nested Fixed Point, 嵌套不动点) 算法是 DDCM 估计的"标准解法"。结构是一个外层优化器 + 内层不动点求解。
5.1 价值函数迭代(Value Function Iteration, VFI)细节
在内层不动点求解中,最常用的是 VFI:
迭代直到 $\|v^{(h+1)} - v^{(h)}\|_\infty < 10^{-8}$。由于 Bellman 算子是压缩映射(压缩率 $\beta < 1$),VFI 保证收敛,但通常需要几百到几千次迭代。改进:Howard policy iteration 每 10-20 次 VFI 做一次 policy improvement,收敛快得多。
变量定义:$T_\theta$ 为 Bellman 压缩算子。识别/收敛条件:$\beta<1$ 是压缩率的来源,Banach 不动点定理保证 $v^*$ 唯一且 VFI 几何收敛。经济直觉:$\beta$ 越小(越短视)收敛越快;这也是为什么动态结构模型把 $\beta$ 校准而非估计。
当状态空间 $\mathcal{X}$ 很大(例如多维状态 $x = (mileage, age, wage)$,$N_x = 10^5$)时,内层 VFI 每次都要在 $N_x \times N_x$ 上做矩阵乘法,外层优化器又要调用内层几百次——总计算量爆炸。这就是 Hotz-Miller (1993) 出现的动机。
06 Hotz-Miller 方法:避免全解
当动态模型含连续状态或均衡约束、似然写不出来时,可用模拟矩估计(SMM)匹配模拟矩。不熟悉?先学 基础知识库·非线性GMM/SMM →;想把 Bellman 约束一次性求解,见 基础知识库·MPEC →。
6.1 核心思想
Hotz and Miller (1993 RES) 提出了一个两步法:不需要在每次外层迭代时都全解 Bellman 方程,而是先用非参数/半参数方法从数据中直接估计 CCP,再用 CCP 反推价值函数,最后做一步 GMM/ML。
6.2 CCP 逆推公式(CCP inversion)
利用 log-sum-exp 的解析结构,Hotz-Miller 证明了一个关键恒等式:在 CCP $P(d|x)$ 和选择特定价值 $v(x,d)$ 之间存在一一对应关系。特别地,如果把"参考选择"(如 $d=0$)的价值设为 0 作归一化,那么:
推导:由 Equation 3.3,$P(d|x) = e^{v(x,d)} / \sum_{d'} e^{v(x,d')}$,于是 $\log P(d|x) = v(x,d) - \log \sum_{d'} e^{v(x,d')}$。两边对 $d=0$ 作差,log-sum-exp 项消掉,得到 Equation 6.1。
变量定义:$\hat P(x_t\mid x;\hat\theta)$ 为从数据估计的转移密度;$\hat v$ 由 Eq.6.1 从 $\hat P(d|x)$ 反推。识别条件:正向模拟用数据中的 $\hat P$ 替代 Bellman 迭代,故只需一步估计、无需内层不动点。经济直觉:HM 的精髓是"CCP 已包含最优策略的全部信息",从观测选择频率即可反推未来价值,这把 DDCM 从计算密集变成两步回归。
6.3 两步估计流程
6.4 优点与局限
- 优点:计算量从"外循环 × 内层不动点"降到"一次非参数估计 + 一次低维优化",特别适合高维状态空间。
- 局限:(a) 第一阶段需要数据在每个状态 $x$ 上都有足够多的观测—— curse of dimensionality;(b) 反事实分析需要重新解 Bellman,所以"估计快"不等于"反事实快";(c) 标准误需要考虑第一阶段非参数估计的不确定性(kernel bandwidth bias)。
07 完整 Python 实现(Rust 例子)
下面用 Rust (1987) 的 bus engine replacement 简化版本演示 NFXP 算法。我们实现:(1) 状态离散化;(2) Bellman 算子;(3) VFI;(4) MLE 外层优化。
"""
Rust (1987) Bus Engine Replacement —— NFXP 简化实现
====================================================
状态: x in {0, 1, ..., NX-1} 表示里程分箱
选择: d in {0,1} = {继续使用, 更换发动机}
当期效用:
u(x, 0) = - theta1 * log(1 + x / theta2) # 运行成本(负号表示成本)
u(x, 1) = - RC # 重置成本(里程归 0)
转移:
d=0: x' ~ Poisson(1) 加在 x 上(截断在 NX-1)
d=1: x' = 0 + Poisson(1)
"""
import numpy as np
from scipy.optimize import minimize_scalar, minimize
from scipy.stats import poisson
# ---------- 1. 模型设定 ----------
NX = 90 # 状态数(里程分箱)
BETA = 0.95 # 贴现因子
RC = 11.7255 # 重置成本(校准值,Rust 论文里的量级)
# 真实参数(用于模拟数据)
theta1_true, theta2_true = 2.0, 10.0
# 状态向量
x = np.arange(NX)
# 转移概率矩阵 P[d] 是 (NX, NX): P[d][x, x_next] = Pr(x_next | x, d)
def build_transition(NX):
P = [np.zeros((NX, NX)), np.zeros((NX, NX))]
# 里程增加服从 Poisson(lambda=1)
lam = 1.0
k_max = 5
pk = poisson.pmf(np.arange(k_max+1), lam)
for xi in range(NX):
for k in range(k_max+1):
# d=0: 里程从 xi 增加 k
xn0 = min(xi + k, NX - 1)
P[0][xi, xn0] += pk[k]
# d=1: 里程从 0 增加 k
xn1 = min(k, NX - 1)
P[1][xi, xn1] += pk[k]
# 归一化(截断尾部)
P[0] /= P[0].sum(axis=1, keepdims=True)
P[1] /= P[1].sum(axis=1, keepdims=True)
return P
P = build_transition(NX)
# ---------- 2. 当期效用 ----------
def flow_utility(theta1, theta2):
"""返回 (NX, 2) 的 flow utility 矩阵"""
u0 = - theta1 * np.log(1 + x / theta2) # 继续使用
u1 = - RC * np.ones(NX) # 更换发动机
return np.column_stack([u0, u1])
# ---------- 3. Bellman 算子 + VFI ----------
def bellman_operator(V, theta1, theta2):
"""
V: (NX, 2) 上一期的 choice-specific value
返回新的 V
"""
u = flow_utility(theta1, theta2) # (NX, 2)
# ex-ante value: EV(x) = logsumexp over d of V(x, d)
V_max = V.max(axis=1, keepdims=True) # (NX, 1)
EV = (V_max.squeeze(1) + np.log(np.exp(V - V_max).sum(axis=1))) # (NX,)
# 对每个 d: V_new(x, d) = u(x,d) + beta * sum_x' P[d][x,x'] * EV(x')
V_new = np.zeros_like(V)
for d in [0, 1]:
EV_next = P[d] @ EV # (NX,)
V_new[:, d] = u[:, d] + BETA * EV_next
return V_new
def solve_bellman(theta1, theta2, tol=1e-9, max_iter=5000):
V = np.zeros((NX, 2))
for it in range(max_iter):
V_new = bellman_operator(V, theta1, theta2)
diff = np.max(np.abs(V_new - V))
V = V_new
if diff < tol:
break
# 计算 CCP
V_max = V.max(axis=1, keepdims=True)
P_choice = np.exp(V - V_max) / np.exp(V - V_max).sum(axis=1, keepdims=True)
return V, P_choice
# ---------- 4. 模拟数据 ----------
def simulate_data(P_choice, T=50, N=200, seed=0):
"""模拟 N 辆 bus 各 T 期的 (x_t, d_t)"""
rng = np.random.default_rng(seed)
data = []
for i in range(N):
xi = 0
for t in range(T):
# 根据 P_choice[xi, :] 抽 d
d = rng.choice(2, p=P_choice[xi])
data.append((xi, d))
# 状态转移
xn = rng.choice(NX, p=P[d][xi])
xi = xn
return np.array(data)
V_true, P_true = solve_bellman(theta1_true, theta2_true)
data = simulate_data(P_true, T=50, N=200)
print(f"模拟数据形状: {data.shape}")
print(f"更换率 (d=1 的比例): {data[:,1].mean():.3f}")
# ---------- 5. 似然函数 + MLE ----------
def neg_loglik(params, data):
theta1, theta2 = params
if theta2 <= 0:
return 1e10
try:
V, P_choice = solve_bellman(theta1, theta2)
except Exception:
return 1e10
# 似然: log P(d | x)
x_obs = data[:, 0].astype(int)
d_obs = data[:, 1].astype(int)
p_obs = P_choice[x_obs, d_obs]
p_obs = np.clip(p_obs, 1e-300, 1.0)
nll = -np.sum(np.log(p_obs))
return nll
# 外层优化
print("\n开始 MLE 估计 ...")
res = minimize(neg_loglik,
x0=[1.0, 5.0],
args=(data,),
method='Nelder-Mead',
options={'xatol':1e-4, 'fatol':1e-4, 'maxiter':50})
print(f"收敛: {res.message}")
print(f"估计 theta1 = {res.x[0]:.3f} (真实 {theta1_true})")
print(f"估计 theta2 = {res.x[1]:.3f} (真实 {theta2_true})")
# ---------- 6. 用估计参数画 CCP ----------
V_hat, P_hat = solve_bellman(*res.x)
print("\n更换概率 P(d=1 | x) 在不同里程分箱:")
for xi in [0, 10, 30, 50, 70, 89]:
print(f" x={xi:3d} P(replace)={P_hat[xi,1]:.4f}")
这段代码完整实现了 Rust (1987) 的 NFXP 算法:(1) 构造转移矩阵;(2) Bellman 算子 + VFI 解内层不动点;(3) 外层 Nelder-Mead 优化做 MLE。在普通笔记本上几秒内即可收敛。完整的学术论文级实现还需要:(a) 用真实 MMB 数据;(b) 标准误计算(analytic gradient);(c) 与 Hotz-Miller 两步法对比;(d) 反事实政策模拟(见下一页)。
07+ 数据来源对照表(动态结构模型常用数据集)
动态结构模型需要面板数据:同一个体/企业多期的状态 $x_t$ 与选择 $d_t$。下表对照经典与中国数据集:
| 数据集 | 国家/地区 | 典型动态选择 | 状态变量 | 获取 |
|---|---|---|---|---|
| Rust MMB Bus | 美国 | 是否更换公交车发动机 | 累计里程、机龄 | Rust (1987) 原始数据(教学基准) |
| NLSY | 美国 | 就业/职业/教育动态选择 | 教育、经验、工资 | NLSY79 公网免费 |
| PSID | 美国 | 迁移、生育、劳动参与 | 家庭资产、孩子数 | PSID 官网注册免费 |
| CFPS | 中国 | 教育、职业、退休动态 | 家庭背景、收入、健康 | 中国家庭追踪调查(北大) |
| CHARLS | 中国 | 退休、健康、消费动态 | 年龄、财富、健康 | 中国健康与养老追踪调查 |
| 工商企业库 | 中国 | 企业进入/退出、投资 | 资本、年龄、利润 | 工业企业数据库(需申请) |
静态离散选择只要"一期横截面";动态结构必须有多期面板,且状态变量要能观察/可构造。Rust (1987) 的 MMB 公交车数据是教学基准(可公开下载);中国选题用 CFPS/CHARLS 做生命周期动态,或用工商企业库做企业投资/退出动态。
08 论文案例
09 常见错误与陷阱
很多学生把 $\beta$ 当自由参数估计,结果发现它和 flow utility 参数高度共线,似然曲面平坦,$\hat\beta$ 收敛到 0 或 1。实践中通常把 $\beta$ 校准为 0.95 或 0.99(对应年化 5% 或 1% 利率),不参与估计。Rust (1987) 原文也是这么做的。
把 bus 里程只分成 10 个 bins,看似节省计算,但 CCP 对状态的敏感性被平滑掉了,反事实政策分析完全失真。状态空间要细到"政策变化不会跨越 bin",通常 $N_x \geq 50$–$200$。
只有当 $\varepsilon_{d}$ 是 i.i.d. Type-I Extreme Value 时,log-sum-exp 公式才有解析形式,CCP 才是简单的 logit。如果改成正态分布(probit),内层不动点每次都要对多元正态数值积分,计算量爆炸。除非有特别理由,默认用 Extreme Value。
如果某些 $x$ 上数据很少(例如 bus 里程很高的罕见观测),$\hat P(d|x)$ 的方差极大,反推的 $\hat v$ 不可靠。需要做 smoothing(kernel / sieve)或合并相邻状态。
不同 bus 经理的"耐心"或"更换成本"可能是固定效应(fixed effect),如果忽略,CCP 估计会有偏。Rust 原文的早期版本就被 Heckman 等人批评过,后续文献用 mixed logit / finite mixture 处理。
10 进阶资料
- Rust (1994, Handbook of Econometrics Vol. IV) — Structural estimation of Markov decision processes. 教科书级综述。
- Aguirregabiria & Mira (2010, Journal of Econometrics) — Dynamic discrete choice structural models: A survey. 现代综述,包含 NPL 算法。
- Keane, Todd, Wolpin (2011) — The Structural Estimation of Behavioral Models. 三本手册章节合集,覆盖劳动、家庭、产业组织应用。
- Arcidiacono, Bayer, Blevins, Ellickson (2016) — Estimation of dynamic discrete choice models using Euler equations. 最新计算方法。
- Python 实现:open-source 包
dcegm、respy(IAB)提供了 Rust 模型和更复杂生命周期模型的完整实现。 - 中文教材:蔡洪滨、孙宁等在《计量经济学》研究生教材中对 Rust 模型有中文讲解,可作为入门对照。
方程总清单 / Equation Summary
本页全部方程按出现顺序汇总如下,共 10 个。每个方程均可在正文中找到对应的变量定义、设定理由与经济直觉。
| 编号 | 方程名称 | 核心公式 | 所在节 |
|---|---|---|---|
| Eq.05-01 | 期望贴现效用(目标函数) | $\max E[\sum_t\beta^t(u(x_t,d_t)+\varepsilon_{t,d_t})]$ | 02.2 |
| Eq.05-02 | Rust 条件独立假设(补全) | $p(x',\varepsilon'\mid x,d)=p(x'\mid x,d)g(\varepsilon')$ | 02.3(补全) |
| Eq.05-03 | Bellman 方程 | $V(x,\varepsilon)=\max_d\{u(x,d)+\varepsilon_d+\beta E[V(x',\varepsilon')\mid x,d]\}$ | 03.1 |
| Eq.05-04 | Choice-specific value | $v(x,d)=u(x,d)+\beta\int\log\sum_{d'}e^{v(x',d')}p(x'\mid x,d)dx'$ | 03.2 |
| Eq.05-05 | CCP 条件选择概率 | $P(d\mid x;\theta)=e^{v(x,d)}/\sum_{d'}e^{v(x,d')}$ | 03.3 |
| Eq.05-06 | Rust 完全似然(补全) | $\ell(\theta)=\sum_{i,t}\log P(d_{it}\mid x_{it};\theta)$ | 04.2(补全) |
| Eq.05-07 | Bellman 算子 $T_\theta$ 与压缩(补全) | $(T_\theta v)(x,d)=u+\beta\int\log\sum_{d'}e^{v(x',d')}p$;$\|T_\theta v_1-T_\theta v_2\|\le\beta\|v_1-v_2\|$ | 05.1(补全) |
| Eq.05-08 | VFI 迭代 | $v^{h+1}(x,d)=u(x,d)+\beta\sum_{x'}p(x'\mid x,d)\log\sum_{d'}e^{v^h(x',d')}$ | 05.1 |
| Eq.05-09 | Hotz-Miller CCP 反转 | $v(x,d)-v(x,0)=\log P(d\mid x)-\log P(0\mid x)$ | 06.2 |
| Eq.05-10 | HM 正向模拟值函数(补全) | $\widehat{EV}(x)=\hat v(x,0)+\sum_t\beta^t\int[\hat u+\log\sum_d e^{\hat v}]d\hat P$ | 06.2(补全) |