欢迎光临寒舍

动态优化结构估计大全

5154 字

s l

动态优化结构估计大全

动态优化结构估计大全

第一章:结构 vs 约化式——为什么要"多此一举"?

1.1 一个直观的例子:出租车司机的劳动供给

想象你在研究网约车司机的劳动供给问题。

约化式(Reduced-form)做法:

1
2
回归:工作小时数 = α + β × 当日价格补贴 + ε
发现:β = 0.5(补贴每增加10%,工作时间增加5%)

这告诉你"有相关性",甚至在好的研究设计下是"因果效应"。但如果政策制定者问你:

“如果我们把补贴从目前的 20%降到 10%,再引入一个’连续工作 4 小时奖励 50 元’的阶梯激励,司机行为会怎么变?”

约化式估计无法回答——因为你估计的是特定补贴形式下的边际效应,而新政策结构完全不同。

结构估计(Structural Estimation)做法:

假设司机每小时做决策:继续工作还是回家?

$$V(\text{状态}) = \max\left\{ \underbrace{u(\text{收入}) - c(\text{疲劳})}_{\text{继续工作的效用}}, \quad \underbrace{V_{\text{home}}}_{\text{回家的保留效用}} \right\}$$

你估计的是:

  • 收入的边际效用参数 $\theta_1$
  • 疲劳成本参数 $\theta_2$
  • 保留效用参数 $\theta_3$

有了这些不变的偏好参数(“深层参数”),任何新政策都可以代入模型计算反事实。

1.2 核心区别总结

维度 约化式 结构式
回答的问题 “X 变化时 Y 怎么变?” “为什么会这样变?新情境下怎么变?”
依赖什么 研究设计(IV/RCT/DID) 经济模型(效用/生产函数)
外部效度 仅限相似情境 可外推至模型覆盖的新情境
透明度 高(模型假设少) 低(依赖模型正确性)
适用场景 政策评估、因果识别 机制分析、反事实模拟、福利分析

1.3 何时用结构?

必须用结构的场景:

  1. 反事实涉及从未观察过的政策(如新税制)
  2. 需要计算福利/消费者剩余
  3. 需要理解机制(行为背后的参数)
  4. 数据中有动态决策需要建模

可以不用结构的场景:

  1. 只需要回答"有没有效果"
  2. 有干净的自然实验且政策情境相似
  3. 模型假设太强、数据太弱

自检问题:你的研究问题是否必须知道"为什么"或"换一种政策会怎样"?如果是,结构估计可能是必要的。


第二章:标准工作流——从 0 到 1 的八步清单

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
┌─────────────────────────────────────────────────────────────────────────────┐
                        结构估计工作流                                        
├─────────────────────────────────────────────────────────────────────────────┤
                                                                              
   经济问题   模型设定   数据对接   识别分析                        
                                                                         
                                                                         
   "要回答      "状态/行动    "观测什么?  "哪些variation               
    什么?"     /收益/转移"   缺什么?"    识别什么?"                      
                                                                              
        估计方法选择   数值实现   推断与检验   反事实              
                                                                         
                                                                         
       "MLE/GMM/       "优化器/       "标准误/       "政策模拟/           
        CCP/SMM"      收敛?"        拟合?"        福利?"               
                                                                              
└─────────────────────────────────────────────────────────────────────────────┘

各步核心问题

步骤 解决什么问题 常见坑 自检
① 问题 明确反事实目标 问题太宽泛 能写出具体的反事实句式?
② 模型 写出可解的优化问题 状态空间太大 贝尔曼方程能解吗?
③ 数据 状态/行动观测映射 状态变量不可观测 模型变量都有数据对应?
④ 识别 参数能否被数据确定 参数共线/弱识别 能写出识别等式?
⑤ 估计 选择计算可行的方法 方法与模型不匹配 目标函数写得出来?
⑥ 数值 得到稳定的估计值 收敛到局部最优 多初值结果一致?
⑦ 推断 量化不确定性 标准误低估 bootstrap 结果合理?
⑧ 反事实 回答政策问题 外推超出支持 新政策在数据支撑范围内?

第三章:贯穿式 Toy Model——发动机更换问题

3.1 问题背景

Harold Zurcher 是麦迪逊公交公司的维修主管,每期需要决定:是否更换公交车发动机?

这是 Rust (1987) 的经典设置,是动态离散选择(DDC)的"Hello World"。

3.2 模型完整设定

符号定义

符号 含义 类型
$x_t$ 发动机里程(累计行驶距离) 状态变量(可观测)
$d_t \in \{0, 1\}$ 决策:0=继续使用,1=更换 行动
$\epsilon_t = (\epsilon_{0t}, \epsilon_{1t})$ 决策特定的私人冲击 状态变量(不可观测)
$\beta \in (0,1)$ 折现因子 已知参数
$\theta = (RC, \theta_c)$ 待估参数 结构参数

当期收益函数

$$ u(x_t, d_t, \epsilon_t; \theta) = \begin{cases} -c(x_t; \theta_c) + \epsilon_{0t} & \text{if } d_t = 0 \text{ (继续使用)} \\ -RC - c(0; \theta_c) + \epsilon_{1t} & \text{if } d_t = 1 \text{ (更换发动机)} \end{cases} $$

其中:

  • $c(x; \theta_c) = \theta_{c1} x + \theta_{c2} x^2$ 是维护成本(里程越高成本越大)
  • $RC$ 是更换发动机的固定成本
  • $\epsilon_{dt}$ 是决策者私人信息(Type-I 极值分布)

状态转移

$$ x_{t+1} = \begin{cases} x_t + \Delta x_t & \text{if } d_t = 0 \\ \Delta x_t & \text{if } d_t = 1 \text{ (里程归零)} \end{cases} $$

其中 $\Delta x_t$ 是单期行驶里程,服从离散分布 $g(\cdot)$(从数据估计)。

误差分布假设

$$ \epsilon_{dt} \overset{iid}{\sim} \text{Type-I Extreme Value (Gumbel)} $$

为什么用这个分布?

  • 解析地得出选择概率(Logit 形式)
  • 计算期望最大值有闭式解(log-sum-exp)

3.3 贝尔曼方程

定义条件价值函数(conditional value function)——给定选择 $d$ 时的期望贴现收益:

$$ \bar{v}(x, d; \theta) = u(x, d; \theta) + \beta \mathbb{E}\left[ V(x', \epsilon') \mid x, d \right] $$

其中 $V$ 是选择前价值函数(ex-ante value function):

$$ V(x, \epsilon) = \max_{d \in \{0,1\}} \left\{ \bar{v}(x, d; \theta) + \epsilon_d \right\} $$

关键简化(极值分布的魔力):

由于 $\epsilon$ 服从 Type-I 极值分布,期望最大值有闭式解:

$$ \mathbb{E}_\epsilon[V(x, \epsilon)] = \log\left( \exp(\bar{v}(x, 0)) + \exp(\bar{v}(x, 1)) \right) + \gamma $$

其中 $\gamma \approx 0.5772$ 是欧拉常数。

简化版贝尔曼方程(核心递归式)

定义期望价值函数 $EV(x) \equiv \mathbb{E}_\epsilon[V(x, \epsilon)]$:

$$ EV(x) = \log\left( \sum_{d=0}^{1} \exp\left( u(x,d) + \beta \sum_{x'} g(x' \mid x, d) \cdot EV(x') \right) \right) $$

这是一个不动点方程:$EV = T(EV; \theta)$

3.4 选择概率

给定 $EV(\cdot)$,选择概率有 Logit 形式:

$$ P(d_t = 1 \mid x_t; \theta) = \frac{\exp(\bar{v}(x_t, 1))}{\exp(\bar{v}(x_t, 0)) + \exp(\bar{v}(x_t, 1))} $$

这就是模型与数据的桥梁:我们观察到 $(x_t, d_t)$,用上述概率构造似然函数。

3.5 状态空间离散化(实操必须)

假设里程 $x \in \{0, 1, 2, \ldots, \bar{x}\}$(如 $\bar{x} = 90$,每单位代表 5000 英里)。

转移矩阵 $G$ 是 $(\bar{x}+1) \times (\bar{x}+1)$ 矩阵:

  • $G_{0}(x, x')$ = 不更换时从 $x$ 转移到 $x'$ 的概率
  • $G_{1}(x, x')$ = 更换时从 $0$ 转移到 $x'$ 的概率

自检:你的模型能否写成这种递归形式?状态空间是否有限/可离散化?


第四章:识别——结构估计的灵魂

4.1 识别的本质

问题:数据能否唯一确定参数 $\theta$?

用数学语言:观察到的分布 $P(d \mid x)$ 是否与 $\theta$ 一一对应?

“参数 → 可观察量"的映射思维

1
2
3
4
5
6
7
                识别 = 找到从θ到数据统计量的可逆映射

        θ = (RC, θ_c)  ──映射M──>  可观测的行为变化 P(d|x)
              ↑                           ↑
          结构参数                    实际能估计的

        识别成功 ⟺ M 是单射(不同θ导致不同P)

4.2 Rust 模型中的识别逻辑

识别 $\theta_c$(维护成本参数)

利用的 variation:不同里程下的更换概率差异

观察:

  • 在 $x = 20$ 时,更换概率是 5%
  • 在 $x = 60$ 时,更换概率是 40%

直觉:里程越高,继续使用的期望成本越大,更换变得更吸引人。选择概率对里程的斜率识别 $\theta_c$。

$$ \frac{\partial P(d=1 \mid x)}{\partial x} \approx \theta_{c1} + 2\theta_{c2} x $$

识别 $RC$(更换成本)

利用的 variation:更换概率的水平

如果 $RC$ 很大,即使里程很高也不愿更换(概率曲线整体下移)。

$$ P(d=1 \mid x=0) \text{ 的水平反映 } RC \text{ 相对于维护成本的大小} $$

识别 $\beta$(折现因子)

困难! 这是 DDC 识别的经典难题。

Rust 的做法:假设 $\beta$ 已知(如 $\beta = 0.9999$)。

为什么难识别?

  • $\beta$ 控制"多看重未来”
  • 但"更看重未来"和"高更换成本"可能导致类似行为
  • 需要额外的 variation(如利率变化、政策变化)

4.3 识别失败的五大类型

类型 现象 Rust 模型中的例子 诊断方法
1. 参数共线/同形 不同参数组合给出相同似然 $\beta$ 和 $RC$ 的共线(都影响"是否更换") 画等似然线;检查 Hessian 奇异
2. 状态遗漏 未观测状态与观测状态相关 司机驾驶习惯影响磨损但未观测 控制更多协变量;使用固定效应
3. 支持集不足 数据中缺少关键 variation 所有观测里程都在 30-50 之间 检查状态变量分布覆盖
4. 分布假设驱动识别 换分布后估计值大变 Type-I 极值 vs 正态分布 换分布做敏感性分析
5. 多重均衡 模型有多个均衡,数据只反映一个 (Rust 模型无此问题,但 IO 博弈中常见) 检验均衡唯一性条件

4.4 识别分析的操作清单

  1. 写出识别等式:$\theta = h(P(d \mid x), G(x' \mid x, d))$
  2. 检查 $h$ 是否可逆:有没有两组不同的 $\theta$ 给出相同的 $P$?
  3. 计算 Fisher 信息矩阵:在真实参数处是否正定?
  4. 做 Monte Carlo:生成模拟数据,看能否恢复真实参数
  5. 画似然曲面:是否有平坦区域?

最常见的坑:依赖函数形式假设(如线性成本)来获得识别,但这种"识别"没有经济学内涵。


第五章:估计方法全家桶

5.1 全信息 MLE / NFXP(Nested Fixed Point)

详见MLE 法

核心思想

一句话:在似然函数中"套娃"——每次评估似然都要解一遍贝尔曼方程。

1
2
3
4
5
6
7
外层优化(找θ使似然最大)
    └──> 内层不动点(给定θ,解EV(x; θ))
              └──> 计算P(d|x; θ)
                       └──> 计算对数似然

目标函数

$$ \mathcal{L}(\theta) = \sum_{i=1}^{N} \sum_{t=1}^{T_i} \left[ d_{it} \log P(d=1 \mid x_{it}; \theta) + (1-d_{it}) \log P(d=0 \mid x_{it}; \theta) \right] $$

算法步骤

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
NFXP算法:
─────────────────────────────────────────────────────
输入:数据{(x_it, d_it)},初始参数θ^(0),容差ε_in, ε_out

OUTER LOOP(优化θ):
  while ||∇L(θ)|| > ε_out:

    INNER LOOP(解EV):
      初始化 EV^(0)(x) = 0 for all x
      while ||EV^(k+1) - EV^(k)|| > ε_in:
        EV^(k+1)(x) = T(EV^(k); θ)  # 贝尔曼迭代
      end

    计算选择概率 P(d|x; θ, EV)
    计算似然 L(θ) 和梯度 ∇L(θ)
    更新 θ ← θ + step × direction

  end

输出:θ̂_MLE
─────────────────────────────────────────────────────

数值实现要点

内层迭代(解 EV):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
def solve_EV(theta, beta, G, tol=1e-10, max_iter=1000):
    """
    求解EV的不动点
    theta: (RC, theta_c1, theta_c2)
    G: 转移概率矩阵 [n_x, n_x, 2] (最后维度是action)
    """
    RC, tc1, tc2 = theta
    n_x = G.shape[0]
    x_grid = np.arange(n_x)

    # 当期效用(不含epsilon)
    u0 = -tc1 * x_grid - tc2 * x_grid**2  # 继续使用
    u1 = -RC - tc1 * 0 - tc2 * 0**2        # 更换(里程归零)
    u1 = np.full(n_x, u1)

    EV = np.zeros(n_x)

    for _ in range(max_iter):
        # 条件价值函数
        v0 = u0 + beta * G[:,:,0] @ EV
        v1 = u1 + beta * G[:,:,1] @ EV

        # Log-sum-exp(数值稳定版)
        max_v = np.maximum(v0, v1)
        EV_new = max_v + np.log(np.exp(v0 - max_v) + np.exp(v1 - max_v))

        if np.max(np.abs(EV_new - EV)) < tol:
            break
        EV = EV_new

    return EV

外层优化(似然最大化):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
from scipy.optimize import minimize

def neg_log_likelihood(theta, data, beta, G):
    """
    负对数似然(用于最小化)
    data: DataFrame with columns ['x', 'd']
    """
    EV = solve_EV(theta, beta, G)

    RC, tc1, tc2 = theta
    x = data['x'].values
    d = data['d'].values

    # 计算选择概率
    u0 = -tc1 * x - tc2 * x**2 + beta * EV[x]  # 注意:需要插值或索引
    u1 = -RC + beta * EV[0]  # 更换后里程归零

    # 数值稳定的softmax
    max_u = np.maximum(u0, u1)
    prob1 = np.exp(u1 - max_u) / (np.exp(u0 - max_u) + np.exp(u1 - max_u))
    prob1 = np.clip(prob1, 1e-10, 1-1e-10)  # 避免log(0)

    # 对数似然
    ll = d * np.log(prob1) + (1-d) * np.log(1-prob1)
    return -np.sum(ll)

# 多初值优化
best_result = None
for _ in range(20):
    theta0 = np.random.uniform([5, 0.001, 0], [15, 0.01, 0.001])
    result = minimize(neg_log_likelihood, theta0, args=(data, beta, G),
                      method='L-BFGS-B',
                      bounds=[(0.1, 50), (1e-6, 0.1), (0, 0.01)])
    if best_result is None or result.fun < best_result.fun:
        best_result = result

优缺点

优点 缺点
统计效率最高(MLE 渐近有效) 计算成本高(每次评估似然都解不动点)
无需矩条件选择 对模型设定敏感
自然处理非线性模型 内层不收敛会导致外层失败

5.2 两步法 / CCP 方法(Hotz-Miller 类)

详见CCP 法

(Conditional Choice Probability)

核心思想

一句话:先非参数估计选择概率 $\hat{P}(d \mid x)$,再利用概率反推价值函数,避免解不动点。

关键洞见:选择概率和价值函数之间存在一一映射

$$ P(d=1 \mid x) = \frac{\exp(\bar{v}(x,1))}{\exp(\bar{v}(x,0)) + \exp(\bar{v}(x,1))} $$

反过来(利用 Type-I 极值分布性质):

$$ \bar{v}(x,1) - \bar{v}(x,0) = \log\left(\frac{P(d=1 \mid x)}{P(d=0 \mid x)}\right) $$

这意味着不需要解贝尔曼方程,只要有 CCP 估计就能构造似然/矩条件。

概率反演(Hotz-Miller Inversion)

定义选择特定值函数(choice-specific value function)的差:

$$ \Delta \bar{v}(x) \equiv \bar{v}(x,1) - \bar{v}(x,0) = \log\left(\frac{p_1(x)}{1-p_1(x)}\right) $$

进一步,可以证明:

$$ \bar{v}(x, d) = u(x, d; \theta) + \beta \sum_{x'} g(x' \mid x, d) \cdot [\bar{v}(x', 0) + \log(1-p_1(x')) + \gamma] $$

这里 $\bar{v}(x', 0) + \log(1-p_1(x'))$ 可以用 $p_1$ 表示(不需要知道绝对水平)。

算法步骤

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
CCP两步法:
─────────────────────────────────────────────────────
第一步:估计"可约化对象"(不涉及结构参数)
  (a) 估计CCP:p̂₁(x) = #{d=1 且 x=x} / #{x=x}  # 或用Logit平滑
  (b) 估计转移概率:Ĝ(x'|x,d) = 频率计数 / 非参数估计

第二步:估计结构参数(单次优化)
  构造伪似然:
    L̃(θ) = Σ [d·log p̃₁(x;θ,p̂) + (1-d)·log(1-p̃₁(x;θ,p̂))]

  其中 p̃₁ 是用θ和p̂构造的"伪CCP"

  最大化 L̃(θ) 得到 θ̂

─────────────────────────────────────────────────────

具体实现:前向模拟版本(Forward Simulation)

有一种更直观的 CCP 方法(Hotz-Miller-Wise 风格):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
def ccp_estimator(data, beta, n_forward=100):
    """
    CCP两步估计器(前向模拟版本)
    """
    # 第一步:估计CCP和转移概率
    ccp = data.groupby('x')['d'].mean().values  # 简单频率估计
    ccp = np.clip(ccp, 0.001, 0.999)  # 避免边界问题

    # 估计里程增量分布
    delta_x_dist = estimate_transition(data)  # 假设已实现

    # 第二步:构造前向价值并估计
    def pseudo_likelihood(theta):
        RC, tc1, tc2 = theta
        n_x = len(ccp)

        # 模拟前向价值
        V_forward = np.zeros(n_x)
        for x in range(n_x):
            # 模拟从x出发、使用当前CCP的期望贴现效用
            V_forward[x] = simulate_forward_value(x, ccp, theta, beta,
                                                   delta_x_dist, n_forward)

        # 构造伪CCP
        u0 = -tc1 * np.arange(n_x) - tc2 * np.arange(n_x)**2
        u1 = -RC + np.zeros(n_x)

        v0 = u0 + beta * expected_continuation(V_forward, 0, delta_x_dist)
        v1 = u1 + beta * V_forward[0]  # 更换后从0开始

        pseudo_ccp = 1 / (1 + np.exp(v0 - v1))

        # 伪似然
        x = data['x'].values
        d = data['d'].values
        ll = d * np.log(pseudo_ccp[x]) + (1-d) * np.log(1-pseudo_ccp[x])
        return -np.sum(ll)

    result = minimize(pseudo_likelihood, [10, 0.005, 0.0001], method='Nelder-Mead')
    return result.x

优缺点

优点 缺点
避免内层不动点迭代 第一步估计误差传递到第二步
计算更快 需要足够数据估计 CCP
对初值不敏感 标准误计算更复杂(需考虑两步)
易于并行化 某些反事实需要重新解不动点

5.3 GMM / SMM(模拟矩方法)

核心思想

GMM 直觉:选择一组矩条件(观测统计量 = 模型预测),然后最小化矩匹配误差。

SMM 直觉:当矩无法解析计算时,用模拟来近似。

矩条件的选择

对于 Rust 模型,自然的矩条件:

  1. 选择矩:$E[d_t - P(d=1 \mid x_t; \theta)] = 0$
  2. 选择 × 状态矩:$E[(d_t - P(d=1 \mid x_t; \theta)) \cdot x_t] = 0$
  3. 转移矩:$E[\mathbf{1}(x_{t+1} = x') - G(x' \mid x_t, d_t)] = 0$
  4. 更换后里程矩:$E[x_{t+1} \mid d_t = 1] = E[\Delta x]$

目标函数

$$ \hat{\theta}_{GMM} = \arg\min_\theta \left[ \hat{g}(\theta) \right]' W \left[ \hat{g}(\theta) \right] $$

其中:

  • $\hat{g}(\theta) = \frac{1}{N} \sum_i m(x_i, d_i; \theta) - \bar{m}^{data}$ 是样本矩
  • $W$ 是权重矩阵

SMM:当矩需要模拟

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
SMM算法:
─────────────────────────────────────────────────────
for each θ in optimization:
    1. 解模型(可选:解不动点或用CCP)
    2. 模拟S条路径,每条长度T
    3. 计算模拟矩:m̂_sim(θ) = (1/S)Σ m(x^s, d^s)
    4. 计算矩差:g(θ) = m̂_data - m̂_sim(θ)
    5. 计算目标:Q(θ) = g(θ)' W g(θ)

最小化 Q(θ) 得到 θ̂_SMM
─────────────────────────────────────────────────────

代码实现

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
def smm_objective(theta, data_moments, beta, G, n_sim=100, T_sim=200, seed=42):
    """
    SMM目标函数
    data_moments: 从真实数据计算的矩
    """
    np.random.seed(seed)  # 固定模拟随机性(CUE估计时重要)

    # 解模型
    EV = solve_EV(theta, beta, G)
    ccp = compute_ccp(EV, theta, beta)

    # 模拟数据
    sim_d = []
    sim_x = []
    for s in range(n_sim):
        x = 0  # 初始里程
        for t in range(T_sim):
            p1 = ccp[min(x, len(ccp)-1)]
            d = np.random.binomial(1, p1)
            sim_d.append(d)
            sim_x.append(x)
            # 转移
            if d == 1:
                x = sample_delta_x(G)  # 更换后从增量开始
            else:
                x = min(x + sample_delta_x(G), len(ccp)-1)

    sim_x = np.array(sim_x)
    sim_d = np.array(sim_d)

    # 计算模拟矩
    sim_moments = np.array([
        sim_d.mean(),                    # 更换率
        (sim_d * sim_x).mean(),          # 更换×里程
        sim_x.mean(),                    # 平均里程
        ((sim_x > 50) * sim_d).mean(),   # 高里程时的更换率
    ])

    # 矩差(需要匹配维度和类型)
    g = data_moments - sim_moments

    # 目标函数(这里用单位矩阵作为权重)
    return g @ g

优缺点

优点 缺点
灵活,可处理复杂模型 矩选择主观(效率取决于选择)
模拟误差随 S 增加消失 模拟噪声导致目标函数不平滑
过度识别可检验 权重矩阵的选择影响效率

5.4 间接推断(Indirect Inference)

详见间接推断

核心思想

一句话:选择一个"辅助模型"(通常是简单的回归),在数据和模拟数据上分别估计,让两者的估计值尽可能接近。

原理:我不直接匹配矩,而是匹配"在简单模型下的行为"。

算法

  1. 在真实数据上估计辅助模型:例如 Logit 回归 $P(d=1|x) = \Lambda(\alpha + \gamma x)$,得到 $\hat{\psi}^{data} = (\hat{\alpha}, \hat{\gamma})$

  2. 对每个结构参数 $\theta$

    • 模拟数据 $\{x^s, d^s\}$
    • 在模拟数据上估计同样的辅助模型,得到 $\hat{\psi}^{sim}(\theta)$
  3. 目标函数

    $$\hat{\theta}_{II} = \arg\min_\theta \left\| \hat{\psi}^{data} - \hat{\psi}^{sim}(\theta) \right\|^2$$

为什么有用?

  • 辅助模型的估计很稳定(通常是凸优化)
  • 辅助模型的参数是结构参数的"平滑函数"
  • 不需要写出解析矩条件

代码示例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
from sklearn.linear_model import LogisticRegression

def indirect_inference(theta, real_data, beta, G, n_sim=50):
    """
    间接推断目标函数
    """
    # 在真实数据上估计辅助模型
    aux_model_real = LogisticRegression()
    X_real = real_data[['x', 'x_sq']].values  # x 和 x^2
    y_real = real_data['d'].values
    aux_model_real.fit(X_real, y_real)
    psi_real = np.concatenate([aux_model_real.intercept_, aux_model_real.coef_.flatten()])

    # 模拟数据
    sim_data = simulate_data(theta, beta, G, n_obs=len(real_data)*n_sim)

    # 在模拟数据上估计辅助模型
    aux_model_sim = LogisticRegression()
    X_sim = sim_data[['x', 'x_sq']].values
    y_sim = sim_data['d'].values
    aux_model_sim.fit(X_sim, y_sim)
    psi_sim = np.concatenate([aux_model_sim.intercept_, aux_model_sim.coef_.flatten()])

    # 目标函数
    return np.sum((psi_real - psi_sim)**2)

5.5 贝叶斯方法(最小可行版本)

详见贝叶斯方法

何时考虑贝叶斯?

  • 样本量小,需要正则化
  • 需要参数的完整后验分布(而非点估计)
  • 想自然地量化不确定性

基本框架

后验 $\propto$ 似然 $\times$ 先验

$$ p(\theta \mid \text{data}) \propto \mathcal{L}(\text{data} \mid \theta) \cdot \pi(\theta) $$

MCMC 实现

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import pymc as pm
import pytensor.tensor as pt

def bayesian_rust_estimation(data, beta, G, n_draws=5000, n_tune=1000):
    """
    贝叶斯NFXP(使用PyMC)
    注意:这是简化版本,真实应用需要更细致的处理
    """
    with pm.Model() as model:
        # 先验
        RC = pm.HalfNormal('RC', sigma=20)
        tc1 = pm.HalfNormal('tc1', sigma=0.01)
        tc2 = pm.HalfNormal('tc2', sigma=0.001)

        # 似然(需要用pytensor实现EV求解器——这里简化处理)
        # 实际中,可能需要用pm.Potential + 自定义似然
        theta = pt.stack([RC, tc1, tc2])

        # 假设有函数 compute_ll_pytensor 用pytensor实现
        ll = compute_ll_pytensor(theta, data, beta, G)
        pm.Potential('likelihood', ll)

        # 采样
        trace = pm.sample(n_draws, tune=n_tune, cores=4)

    return trace

实用建议

  • 先验选择:使用弱信息先验(如半正态、对数正态),检验先验敏感性
  • 收敛诊断:检查 $\hat{R}$(<1.01)、有效样本量(ESS > 400)、trace plot
  • 计算挑战:贝叶斯 + NFXP 很慢,考虑用近似贝叶斯(如变分推断)

第六章:数值实现与工程细节

6.1 状态空间处理

离散化策略

方法 适用场景 实现
均匀网格 有界状态 np.linspace(x_min, x_max, n)
切比雪夫节点 函数逼近 避免龙格现象
内生网格法 连续选择问题 EGM (Carroll, 2006)
状态依赖网格 密度不均匀 在高密度区加密
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
def discretize_state(n_points=100, x_max=450, method='uniform'):
    """
    状态空间离散化(里程)
    """
    if method == 'uniform':
        return np.linspace(0, x_max, n_points)
    elif method == 'chebyshev':
        # 切比雪夫节点,适合插值
        k = np.arange(1, n_points+1)
        nodes = 0.5 * (x_max) * (1 - np.cos((2*k - 1) * np.pi / (2*n_points)))
        return nodes

转移矩阵构造

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def construct_transition_matrix(x_grid, delta_x_probs, x_max):
    """
    构造转移概率矩阵
    delta_x_probs: 里程增量的概率分布(如 [p_0, p_1, p_2])
    """
    n_x = len(x_grid)
    n_delta = len(delta_x_probs)

    # 不更换时的转移矩阵
    G0 = np.zeros((n_x, n_x))
    for i, x in enumerate(x_grid):
        for delta, p in enumerate(delta_x_probs):
            new_x = x + delta
            j = np.searchsorted(x_grid, new_x)  # 找最近的网格点
            j = min(j, n_x - 1)  # 边界处理
            G0[i, j] += p

    # 更换时的转移矩阵(从0开始)
    G1 = np.zeros((n_x, n_x))
    for delta, p in enumerate(delta_x_probs):
        j = min(delta, n_x - 1)
        G1[:, j] = p  # 所有状态转移到相同的里程增量

    return G0, G1

6.2 数值稳定性

Log-Sum-Exp 技巧

问题:$\log(\exp(a) + \exp(b))$ 当 $a, b$ 很大时溢出

解决

$$\log(\exp(a) + \exp(b)) = \max(a, b) + \log(1 + \exp(-|a-b|))$$
1
2
3
4
5
6
7
def logsumexp_stable(a, b):
    """数值稳定的log-sum-exp"""
    max_val = np.maximum(a, b)
    return max_val + np.log1p(np.exp(-np.abs(a - b)))

# 或使用scipy
from scipy.special import logsumexp

概率裁剪

1
2
3
def safe_prob(p, eps=1e-10):
    """避免log(0)和log(1)"""
    return np.clip(p, eps, 1 - eps)

6.3 优化器选择与初值策略

优化器对比

优化器 适用场景 scipy 实现
L-BFGS-B 光滑、有界约束 method='L-BFGS-B'
Nelder-Mead 非光滑、无梯度 method='Nelder-Mead'
BFGS 光滑、无约束 method='BFGS'
差分进化 全局优化 scipy.optimize.differential_evolution
Basin-hopping 多局部最优 scipy.optimize.basinhopping

多初值策略

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
def multi_start_optimization(objective, bounds, n_starts=20, method='L-BFGS-B'):
    """
    多初值优化
    """
    results = []

    # 拉丁超立方采样初值
    from scipy.stats.qmc import LatinHypercube
    sampler = LatinHypercube(d=len(bounds))
    samples = sampler.random(n=n_starts)

    for i in range(n_starts):
        # 将[0,1]映射到bounds
        x0 = np.array([b[0] + samples[i,j]*(b[1]-b[0])
                       for j, b in enumerate(bounds)])

        try:
            res = minimize(objective, x0, method=method, bounds=bounds)
            results.append(res)
        except:
            continue

    # 选择最优
    best = min(results, key=lambda r: r.fun)

    # 检查收敛一致性
    top_3 = sorted(results, key=lambda r: r.fun)[:3]
    print("Top 3 function values:", [r.fun for r in top_3])
    print("Top 3 parameters:", [r.x for r in top_3])

    return best

6.4 收敛诊断

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
def diagnose_convergence(result, objective, bounds, n_restart=5):
    """
    诊断优化收敛质量
    """
    diagnostics = {}

    # 1. 检查梯度是否接近零
    from scipy.optimize import approx_fprime
    grad = approx_fprime(result.x, objective, 1e-8)
    diagnostics['grad_norm'] = np.linalg.norm(grad)

    # 2. 检查Hessian正定性
    from numdifftools import Hessian
    hess_func = Hessian(objective)
    H = hess_func(result.x)
    eigenvalues = np.linalg.eigvalsh(H)
    diagnostics['hessian_positive_definite'] = np.all(eigenvalues > 0)
    diagnostics['min_eigenvalue'] = eigenvalues.min()

    # 3. 从最优解附近重新优化
    restart_results = []
    for _ in range(n_restart):
        x0 = result.x + np.random.normal(0, 0.01, size=len(result.x))
        x0 = np.clip(x0, [b[0] for b in bounds], [b[1] for b in bounds])
        res = minimize(objective, x0, method='L-BFGS-B', bounds=bounds)
        restart_results.append(res.fun)

    diagnostics['restart_variation'] = np.std(restart_results)

    return diagnostics

6.5 并行化

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
from joblib import Parallel, delayed

def parallel_smm(thetas, data_moments, beta, G, n_jobs=-1):
    """
    并行计算多个参数点的SMM目标值
    """
    def eval_one(theta):
        return smm_objective(theta, data_moments, beta, G)

    results = Parallel(n_jobs=n_jobs)(
        delayed(eval_one)(theta) for theta in thetas
    )

    return np.array(results)

第七章:标准误与统计推断

7.1 各估计器的标准误

NFXP/MLE 标准误

理论:$\sqrt{N}(\hat{\theta} - \theta_0) \xrightarrow{d} N(0, \mathcal{I}(\theta_0)^{-1})$

计算

$$\hat{Var}(\hat{\theta}) = \left[ -\frac{1}{N} \sum_i \frac{\partial^2 \log \mathcal{L}_i}{\partial \theta \partial \theta'} \right]^{-1}$$
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
def compute_mle_se(theta_hat, neg_loglik, data):
    """
    MLE标准误(基于Hessian)
    """
    from numdifftools import Hessian

    # 数值Hessian
    hess_func = Hessian(lambda t: neg_loglik(t, data))
    H = hess_func(theta_hat)

    # 信息矩阵 = -Hessian / N
    I_hat = H / len(data)

    # 方差 = 信息矩阵逆 / N
    try:
        var_theta = np.linalg.inv(I_hat) / len(data)
        se = np.sqrt(np.diag(var_theta))
    except np.linalg.LinAlgError:
        print("Warning: Hessian is singular!")
        se = np.full(len(theta_hat), np.nan)

    return se

两步法标准误(Murphy-Topel 修正)

问题:第一步估计的不确定性需要传递到第二步。

公式

$$Var(\hat{\theta}_2) = V_2 + V_2 \cdot C \cdot V_1 \cdot C' \cdot V_2$$

其中 $V_1$ 是第一步方差,$V_2$ 是忽略第一步时的方差,$C$ 是交叉项。

GMM/SMM 标准误

公式

$$\hat{Var}(\hat{\theta}) = \frac{1}{N}(D'WD)^{-1} D'W \hat{\Omega} W D (D'WD)^{-1}$$

其中 $D = \partial g / \partial \theta'$,$\hat{\Omega}$ 是矩条件的方差。

7.2 Bootstrap

推荐做法:参数化 bootstrap 或非参数 bootstrap

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
def bootstrap_se(data, estimator_func, n_bootstrap=200, n_jobs=-1):
    """
    Bootstrap标准误
    """
    def one_bootstrap(seed):
        np.random.seed(seed)
        # 重抽样
        idx = np.random.choice(len(data), size=len(data), replace=True)
        boot_data = data.iloc[idx]
        # 估计
        try:
            return estimator_func(boot_data)
        except:
            return np.full(len(data.columns), np.nan)

    results = Parallel(n_jobs=n_jobs)(
        delayed(one_bootstrap)(s) for s in range(n_bootstrap)
    )

    results = np.array([r for r in results if not np.any(np.isnan(r))])

    se = np.std(results, axis=0)
    ci_low = np.percentile(results, 2.5, axis=0)
    ci_high = np.percentile(results, 97.5, axis=0)

    return se, ci_low, ci_high

7.3 标准误失真的常见原因

原因 症状 解决方案
Hessian 数值不稳定 SE 过大或为 NaN 用更精细的差分步长;改用 BHHH
弱识别 SE 巨大、CI 包含无意义值 检查识别条件;加数据
聚类相关 SE 低估、t 值过大 聚类 bootstrap
边界解 SE 无意义 用 profile likelihood

第八章:模型检验与稳健性

8.1 模型拟合检验

视觉检验

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def plot_model_fit(data, theta_hat, beta, G):
    """
    可视化模型拟合
    """
    import matplotlib.pyplot as plt

    # 计算模型预测的CCP
    EV = solve_EV(theta_hat, beta, G)
    ccp_model = compute_ccp(EV, theta_hat, beta)

    # 数据中的CCP
    ccp_data = data.groupby('x')['d'].mean()

    fig, ax = plt.subplots(figsize=(10, 6))

    ax.scatter(ccp_data.index, ccp_data.values, alpha=0.5, label='Data')
    ax.plot(np.arange(len(ccp_model)), ccp_model, 'r-', linewidth=2, label='Model')

    ax.set_xlabel('Mileage')
    ax.set_ylabel('Replacement Probability')
    ax.legend()
    ax.set_title('Model Fit: Observed vs. Predicted CCP')

    return fig

定量检验

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
def goodness_of_fit(data, theta_hat, beta, G):
    """
    拟合优度检验
    """
    # 模型预测
    EV = solve_EV(theta_hat, beta, G)
    ccp_model = compute_ccp(EV, theta_hat, beta)

    # 计算各类统计量
    x = data['x'].values
    d = data['d'].values

    # 预测概率
    pred_prob = ccp_model[x]

    # Log-likelihood
    ll = np.sum(d * np.log(pred_prob + 1e-10) + (1-d) * np.log(1 - pred_prob + 1e-10))

    # Pseudo R-squared (McFadden)
    ll_null = len(data) * (d.mean() * np.log(d.mean()) + (1-d.mean()) * np.log(1-d.mean()))
    pseudo_r2 = 1 - ll / ll_null

    # 按里程分组的拟合误差
    data['pred'] = pred_prob
    data['x_bin'] = pd.cut(data['x'], bins=10)
    fit_by_bin = data.groupby('x_bin').agg({'d': 'mean', 'pred': 'mean'})
    rmse = np.sqrt(((fit_by_bin['d'] - fit_by_bin['pred'])**2).mean())

    return {
        'log_likelihood': ll,
        'pseudo_r2': pseudo_r2,
        'rmse_by_bin': rmse
    }

8.2 过度识别检验(GMM)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
def j_test(data, theta_hat, moments_func, W):
    """
    J统计量过度识别检验
    """
    N = len(data)
    g = moments_func(theta_hat, data)  # 矩条件向量
    n_moments = len(g)
    n_params = len(theta_hat)

    # J统计量
    J = N * g @ W @ g

    # 自由度 = 过度识别的数量
    df = n_moments - n_params

    # p值
    from scipy.stats import chi2
    p_value = 1 - chi2.cdf(J, df)

    return {'J': J, 'df': df, 'p_value': p_value}

8.3 敏感性分析

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
def sensitivity_analysis(data, baseline_theta, variations):
    """
    对关键假设的敏感性分析
    """
    results = []

    # 基准结果
    baseline_result = estimate_model(data, beta=0.9999)
    results.append({'spec': 'baseline', **dict(zip(['RC', 'tc1', 'tc2'], baseline_result))})

    for var_name, var_value in variations.items():
        if var_name == 'beta':
            result = estimate_model(data, beta=var_value)
        elif var_name == 'cost_functional_form':
            result = estimate_model_alt_cost(data, form=var_value)
        # ... 其他变体

        results.append({'spec': f'{var_name}={var_value}',
                       **dict(zip(['RC', 'tc1', 'tc2'], result))})

    return pd.DataFrame(results)

# 使用示例
variations = {
    'beta': [0.99, 0.999, 0.9999],
    'cost_functional_form': ['linear', 'quadratic', 'cubic'],
    'error_distribution': ['logit', 'probit']
}

8.4 稳健性清单

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
## 稳健性检查清单

### 模型规格

-   [ ] 成本函数形式:线性 vs 二次 vs 非参数
-   [ ] 折现因子:β ∈ {0.95, 0.99, 0.999, 0.9999}
-   [ ] 误差分布:Logit vs Probit

### 数据处理

-   [ ] 去除极端值(<1%, >99%分位)
-   [ ] 不同的状态离散化粒度
-   [ ] 不同的样本期间

### 估计方法

-   [ ] NFXP vs CCP vs GMM 结果比较
-   [ ] 不同初值的收敛性
-   [ ] Bootstrap vs Hessian 标准误

### 拟合质量

-   [ ] 整体更换率拟合
-   [ ] 分里程段的更换率拟合
-   [ ] 样本外预测(如适用)

第九章:反事实——结构估计的终点

9.1 反事实模拟的一般框架

1
2
3
4
5
6
7
8
9
反事实模拟流程:
─────────────────────────────────────────────────────
1. 用估计的 θ̂ 作为真实偏好参数
2. 修改政策环境(如成本函数、转移概率)
3. 在新环境下重新解模型(解新的贝尔曼方程)
4. 计算新均衡下的行为/福利
5. 比较 counterfactual - baseline
6. 报告不确定性(如用 bootstrap)
─────────────────────────────────────────────────────

9.2 反事实案例一:更换成本补贴

问题:如果政府补贴 50%的更换成本,更换率会如何变化?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
def counterfactual_cost_subsidy(theta_hat, beta, G, subsidy_rate=0.5):
    """
    反事实:更换成本补贴
    """
    RC, tc1, tc2 = theta_hat

    # 基准情况
    EV_base = solve_EV(theta_hat, beta, G)
    ccp_base = compute_ccp(EV_base, theta_hat, beta)
    avg_replacement_rate_base = ccp_base.mean()  # 简化:均匀分布状态

    # 反事实:RC降低
    RC_cf = RC * (1 - subsidy_rate)
    theta_cf = (RC_cf, tc1, tc2)

    EV_cf = solve_EV(theta_cf, beta, G)
    ccp_cf = compute_ccp(EV_cf, theta_cf, beta)
    avg_replacement_rate_cf = ccp_cf.mean()

    # 结果
    return {
        'baseline_replacement_rate': avg_replacement_rate_base,
        'counterfactual_replacement_rate': avg_replacement_rate_cf,
        'change': avg_replacement_rate_cf - avg_replacement_rate_base,
        'change_percent': (avg_replacement_rate_cf - avg_replacement_rate_base) / avg_replacement_rate_base * 100
    }

9.3 反事实案例二:维护技术进步

问题:如果新技术使维护成本降低 30%,长期均衡会怎样?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
def counterfactual_tech_improvement(theta_hat, beta, G, cost_reduction=0.3):
    """
    反事实:维护成本降低
    """
    RC, tc1, tc2 = theta_hat

    # 反事实:维护成本参数降低
    tc1_cf = tc1 * (1 - cost_reduction)
    tc2_cf = tc2 * (1 - cost_reduction)
    theta_cf = (RC, tc1_cf, tc2_cf)

    # 解反事实模型
    EV_cf = solve_EV(theta_cf, beta, G)
    ccp_cf = compute_ccp(EV_cf, theta_cf, beta)

    # 模拟长期平稳分布
    stationary_dist = simulate_stationary_distribution(ccp_cf, G, n_sim=100000)

    # 计算平均里程和更换频率
    avg_mileage_cf = np.dot(stationary_dist, np.arange(len(stationary_dist)))
    avg_replacement_rate_cf = np.dot(stationary_dist, ccp_cf)

    return {
        'avg_mileage_counterfactual': avg_mileage_cf,
        'avg_replacement_rate_counterfactual': avg_replacement_rate_cf,
        'stationary_distribution': stationary_dist
    }

def simulate_stationary_distribution(ccp, G, n_sim=100000, T=500):
    """
    模拟长期平稳分布
    """
    G0, G1 = G
    n_x = len(ccp)

    # 构造联合转移矩阵
    # P(x'|x) = p(d=0|x) * G0(x,x') + p(d=1|x) * G1(x,x')
    P = np.zeros((n_x, n_x))
    for x in range(n_x):
        P[x, :] = (1 - ccp[x]) * G0[x, :] + ccp[x] * G1[x, :]

    # 找平稳分布(特征向量法)
    eigenvalues, eigenvectors = np.linalg.eig(P.T)
    idx = np.argmin(np.abs(eigenvalues - 1))
    stationary = np.real(eigenvectors[:, idx])
    stationary = stationary / stationary.sum()

    return stationary

9.4 反事实的不确定性

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
def counterfactual_with_uncertainty(data, estimator_func, cf_func, n_bootstrap=200):
    """
    带不确定性的反事实分析
    """
    # 点估计
    theta_hat = estimator_func(data)
    cf_point = cf_func(theta_hat)

    # Bootstrap
    cf_bootstrap = []
    for b in range(n_bootstrap):
        # 重抽样
        boot_idx = np.random.choice(len(data), size=len(data), replace=True)
        boot_data = data.iloc[boot_idx]

        try:
            theta_b = estimator_func(boot_data)
            cf_b = cf_func(theta_b)
            cf_bootstrap.append(cf_b)
        except:
            continue

    # 整理结果
    cf_array = np.array([list(cf.values()) for cf in cf_bootstrap])

    results = {}
    for i, key in enumerate(cf_point.keys()):
        results[key] = {
            'point': cf_point[key],
            'se': np.std(cf_array[:, i]),
            'ci_low': np.percentile(cf_array[:, i], 2.5),
            'ci_high': np.percentile(cf_array[:, i], 97.5)
        }

    return results

9.5 反事实报告模板

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
## 反事实分析结果

### 情景:更换成本补贴 50%

| 指标 | 基准 | 反事实 | 变化 | 95% CI |
| --- | --- | --- | --- | --- |
| 更换率 | 3.2% | 5.8% | +2.6pp | [2.1, 3.2] |
| 平均里程 | 28.5 | 21.3 | -7.2 | [-9.1, -5.4] |
| 年均更换次数 | 1.2 | 2.0 | +0.8 | [0.6, 1.0] |

### 主要发现

1. **更换率大幅上升**:50%的成本补贴使更换率几乎翻倍(从 3.2%到 5.8%)

2. **车队里程分布左移**:由于更频繁更换,平均里程下降 25%

3. **政策成本**:假设车队规模 100 辆,年补贴支出约为...

### 注意事项

-   反事实假设政策对维护成本无溢出效应
-   未考虑一般均衡效应(如发动机价格变化)
-   置信区间基于 200 次 bootstrap

推荐资源

教材

  • Aguirregabiria & Mira (2010): “Dynamic Discrete Choice Structural Models: A Survey”
  • Keane, Todd & Wolpin (2011): “The Structural Estimation of Behavioral Models”
  • Kenneth Train: “Discrete Choice Methods with Simulation”

经典论文

  • Rust (1987): “Optimal Replacement of GMC Bus Engines”
  • Hotz & Miller (1993): “Conditional Choice Probabilities”
  • Keane & Wolpin (1997): “Career Decisions of Young Men”

代码资源

  • QuantEcon 讲义: https://quantecon.org/
  • Victor Aguirregabiria 的课程网站
  • GitHub: 搜索 “rust model” 或 “DDC estimation”

附录:完整代码框架

完整 py 代码
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
"""
结构估计完整代码框架:Rust (1987) 发动机更换模型
"""

import numpy as np
import pandas as pd
from scipy.optimize import minimize
from scipy.special import logsumexp
from dataclasses import dataclass
from typing import Tuple, Dict, Optional
import matplotlib.pyplot as plt

# ==================== 模型设定 ====================

@dataclass
class RustModel:
    """Rust模型参数配置"""
    n_x: int = 90              # 状态空间大小
    beta: float = 0.9999       # 折现因子
    theta_true: Tuple = (10.0, 0.005, 0.0001)  # (RC, tc1, tc2)

    def __post_init__(self):
        self.x_grid = np.arange(self.n_x)


# ==================== 转移矩阵 ====================

def construct_transition(model: RustModel, delta_probs: np.ndarray) -> Tuple[np.ndarray, np.ndarray]:
    """
    构造转移矩阵
    delta_probs: [p_0, p_1, p_2, ...] 里程增量概率
    """
    n_x = model.n_x
    n_delta = len(delta_probs)

    G0 = np.zeros((n_x, n_x))  # 不更换
    G1 = np.zeros((n_x, n_x))  # 更换

    for x in range(n_x):
        for delta, p in enumerate(delta_probs):
            # 不更换:x -> x + delta
            x_new = min(x + delta, n_x - 1)
            G0[x, x_new] += p

            # 更换:任意x -> delta(归零后的增量)
            G1[x, min(delta, n_x - 1)] += p

    return G0, G1


# ==================== 模型求解 ====================

def solve_EV(theta: Tuple, model: RustModel, G0: np.ndarray, G1: np.ndarray,
             tol: float = 1e-10, max_iter: int = 1000) -> np.ndarray:
    """
    求解期望价值函数 EV(x)
    使用值函数迭代
    """
    RC, tc1, tc2 = theta
    beta = model.beta
    x_grid = model.x_grid
    n_x = model.n_x

    # 当期效用(不含epsilon)
    u0 = -tc1 * x_grid - tc2 * x_grid**2  # 继续使用
    u1 = np.full(n_x, -RC)                  # 更换

    EV = np.zeros(n_x)

    for iteration in range(max_iter):
        # 条件价值函数
        v0 = u0 + beta * G0 @ EV
        v1 = u1 + beta * G1 @ EV

        # Log-sum-exp(数值稳定)
        EV_new = logsumexp(np.stack([v0, v1]), axis=0)

        # 收敛检查
        if np.max(np.abs(EV_new - EV)) < tol:
            return EV_new

        EV = EV_new

    print("Warning: EV did not converge")
    return EV


def compute_ccp(EV: np.ndarray, theta: Tuple, model: RustModel,
                G0: np.ndarray, G1: np.ndarray) -> np.ndarray:
    """
    计算条件选择概率 P(d=1|x)
    """
    RC, tc1, tc2 = theta
    beta = model.beta
    x_grid = model.x_grid

    u0 = -tc1 * x_grid - tc2 * x_grid**2
    u1 = np.full(len(x_grid), -RC)

    v0 = u0 + beta * G0 @ EV
    v1 = u1 + beta * G1 @ EV

    # Logit CCP
    ccp = 1 / (1 + np.exp(v0 - v1))
    return ccp


# ==================== 似然函数 ====================

def neg_log_likelihood(theta: Tuple, data: pd.DataFrame, model: RustModel,
                        G0: np.ndarray, G1: np.ndarray) -> float:
    """
    负对数似然函数(NFXP)
    """
    # 参数约束
    if theta[0] <= 0 or theta[1] <= 0:
        return 1e10

    EV = solve_EV(theta, model, G0, G1)
    ccp = compute_ccp(EV, theta, model, G0, G1)

    x = data['x'].values.astype(int)
    d = data['d'].values

    # 裁剪防止log(0)
    prob = np.clip(ccp[x], 1e-10, 1-1e-10)

    ll = d * np.log(prob) + (1-d) * np.log(1-prob)
    return -np.sum(ll)


# ==================== NFXP估计器 ====================

def nfxp_estimator(data: pd.DataFrame, model: RustModel,
                   G0: np.ndarray, G1: np.ndarray,
                   n_starts: int = 20) -> Dict:
    """
    NFXP估计器(多初值)
    """
    bounds = [(0.1, 50), (1e-6, 0.1), (0, 0.01)]

    best_result = None
    all_results = []

    for i in range(n_starts):
        # 随机初值
        x0 = [
            np.random.uniform(bounds[0][0], bounds[0][1]),
            np.random.uniform(bounds[1][0], bounds[1][1]),
            np.random.uniform(bounds[2][0], bounds[2][1])
        ]

        try:
            result = minimize(
                neg_log_likelihood,
                x0,
                args=(data, model, G0, G1),
                method='L-BFGS-B',
                bounds=bounds
            )
            all_results.append(result)

            if best_result is None or result.fun < best_result.fun:
                best_result = result
        except Exception as e:
            continue

    # 计算标准误
    from numdifftools import Hessian
    hess_func = Hessian(lambda t: neg_log_likelihood(t, data, model, G0, G1))
    H = hess_func(best_result.x)

    try:
        var_matrix = np.linalg.inv(H)
        se = np.sqrt(np.diag(var_matrix))
    except:
        se = np.full(3, np.nan)

    return {
        'theta_hat': best_result.x,
        'se': se,
        'log_likelihood': -best_result.fun,
        'converged': best_result.success,
        'n_iterations': best_result.nit
    }


# ==================== 模拟数据 ====================

def simulate_data(theta: Tuple, model: RustModel,
                  G0: np.ndarray, G1: np.ndarray,
                  n_buses: int = 100, T: int = 120) -> pd.DataFrame:
    """
    模拟面板数据
    """
    EV = solve_EV(theta, model, G0, G1)
    ccp = compute_ccp(EV, theta, model, G0, G1)

    data = []

    for bus in range(n_buses):
        x = 0
        for t in range(T):
            # 选择
            p1 = ccp[min(x, model.n_x - 1)]
            d = np.random.binomial(1, p1)

            data.append({'bus': bus, 't': t, 'x': x, 'd': d})

            # 转移
            if d == 1:
                # 更换:里程归零,加上当期行驶
                delta = np.random.choice(len(G1[0]), p=G1[0])
                x = delta
            else:
                # 继续使用:里程累加
                delta = np.random.choice(len(G0[x]), p=G0[min(x, model.n_x-1)])
                x = min(x + delta, model.n_x - 1)

    return pd.DataFrame(data)


# ==================== 反事实分析 ====================

def counterfactual_analysis(theta_hat: Tuple, model: RustModel,
                            G0: np.ndarray, G1: np.ndarray,
                            rc_change: float = -0.5) -> Dict:
    """
    反事实:更换成本变化
    rc_change: 比例变化(-0.5 表示降低50%)
    """
    # 基准
    EV_base = solve_EV(theta_hat, model, G0, G1)
    ccp_base = compute_ccp(EV_base, theta_hat, model, G0, G1)

    # 反事实
    RC_new = theta_hat[0] * (1 + rc_change)
    theta_cf = (RC_new, theta_hat[1], theta_hat[2])

    EV_cf = solve_EV(theta_cf, model, G0, G1)
    ccp_cf = compute_ccp(EV_cf, theta_cf, model, G0, G1)

    return {
        'baseline_mean_ccp': ccp_base.mean(),
        'counterfactual_mean_ccp': ccp_cf.mean(),
        'ccp_change': ccp_cf.mean() - ccp_base.mean(),
        'baseline_ccp': ccp_base,
        'counterfactual_ccp': ccp_cf
    }


# ==================== 主程序 ====================

if __name__ == "__main__":
    # 设置
    np.random.seed(42)
    model = RustModel()
    delta_probs = np.array([0.3, 0.4, 0.2, 0.1])  # 里程增量分布

    # 构造转移矩阵
    G0, G1 = construct_transition(model, delta_probs)

    # 模拟数据
    print("Simulating data...")
    data = simulate_data(model.theta_true, model, G0, G1, n_buses=100, T=120)
    print(f"Data shape: {data.shape}")
    print(f"Replacement rate: {data['d'].mean():.4f}")

    # 估计
    print("\nEstimating model...")
    result = nfxp_estimator(data, model, G0, G1, n_starts=10)

    print("\nResults:")
    print(f"True parameters:      RC={model.theta_true[0]:.4f}, tc1={model.theta_true[1]:.6f}, tc2={model.theta_true[2]:.8f}")
    print(f"Estimated parameters: RC={result['theta_hat'][0]:.4f}, tc1={result['theta_hat'][1]:.6f}, tc2={result['theta_hat'][2]:.8f}")
    print(f"Standard errors:      RC={result['se'][0]:.4f}, tc1={result['se'][1]:.6f}, tc2={result['se'][2]:.8f}")
    print(f"Log-likelihood: {result['log_likelihood']:.2f}")

    # 反事实
    print("\nCounterfactual Analysis: 50% RC Reduction")
    cf = counterfactual_analysis(result['theta_hat'], model, G0, G1, rc_change=-0.5)
    print(f"Baseline mean CCP: {cf['baseline_mean_ccp']:.4f}")
    print(f"Counterfactual mean CCP: {cf['counterfactual_mean_ccp']:.4f}")
    print(f"Change: {cf['ccp_change']:.4f}")

使用 Hugo 构建
主题 StackJimmy 设计