欢迎光临寒舍

需求函数结构估计大全

4008 字

需求函数的结构方程估计是指通过结构方程模型来估计需求函数的参数。

结构计量经济学:从直觉到代码的完整指南


第一章 什么是结构估计?

参见我另一篇文章的内容:动态优化结构估计大全

任何函数的结构估计都是要回归到动态最优化的形式,因为结构估计的初衷就是估计动态最优化过程。

理论上,只要你能将你的模型挂靠上动态最优化的壳,就可以做结构估计。

1.1 一个直觉性的开场

想象你观察到一个现象:油价上涨后,SUV 销量下降了

简约式(Reduced Form)做法:

1
销量 = α + β × 油价 + ε

跑个回归,得到 $\beta = -0.3$,发表,收工。

结构估计的追问:

  • 这个 -0.3 是从哪里来的?
  • 如果政府补贴电动车,SUV 销量会变多少?
  • 如果通货膨胀导致所有商品价格翻倍,这个弹性还是 -0.3 吗?

简约式回答不了这些问题,因为它只描述了数据中的相关性,没有告诉我们行为机制

1.2 结构 vs 简约:本质区别

维度 简约式 结构估计
目标 描述条件期望 $E[Y \| X]$ 恢复经济行为的深层参数(primitives)
参数含义 统计相关 偏好/技术/信息结构
反事实分析 ❌ 不可信 ✅ 核心优势
模型依赖 高(但可检验)
Lucas 批判免疫

关键洞察:

结构估计的核心是相信:数据是由某个经济模型生成的,我们要做的是"逆向工程"——从数据反推模型参数。

$$ \text{Data} \xleftarrow{\text{DGP}} \text{Model}(\theta) \xrightarrow{\text{估计}} \hat{\theta} $$

1.3 什么时候用结构估计?

适合用:

  • 需要做政策反事实(如:取消某项补贴的福利影响)
  • 关心的参数只能从模型中定义(如:消费者剩余)
  • 需要将局部估计外推到新环境

可以不用:

  • 只需描述因果效应,且有干净的识别策略
  • 模型误设的风险太高,无法验证

⚠️ 常见陷阱: 把结构估计当作"更高级"的方法。实际上它是更强假设换取更多结论的 trade-off。

✅ 自检清单:

  • 我的研究问题必须用结构模型才能回答吗?
  • 我的模型假设是否可以用数据部分检验?

第二章 结构模型的三要素

任何结构模型都可以拆解为三个模块:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
┌─────────────────────────────────────────────────────────┐
│                    结构模型架构                           │
├──────────────┬──────────────────┬───────────────────────┤
│  Primitives  │   Equilibrium    │     Observables       │
│  (深层参数)   │   (均衡条件)      │     (可观测量)         │
├──────────────┼──────────────────┼───────────────────────┤
│ 偏好参数      │ 一阶条件          │ 价格、数量、份额       │
│ 技术参数      │ 市场出清          │ 进入退出决策          │
│ 信息结构      │ 纳什均衡          │ 投资、选择概率        │
│ 分布假设      │ 完美预期/理性预期  │ ...                  │
└──────────────┴──────────────────┴───────────────────────┘

2.1 Primitives:你要估的东西

这些是模型的深层参数,理论上在政策变化时保持稳定。

例:离散选择需求模型

消费者 $i$ 对产品 $j$ 的效用:

$$ u_{ij} = \underbrace{\beta_i' x_j}_{\text{口味×属性}} - \underbrace{\alpha_i \cdot p_j}_{\text{价格敏感度}} + \underbrace{\xi_j}_{\text{未观测质量}} + \underbrace{\varepsilon_{ij}}_{\text{个体异质}} $$

要估计的 primitives:

  • $\alpha$:价格系数(用于计算弹性)
  • $\beta$:属性偏好
  • $\xi_j$:产品固定效应(与内生性紧密相关)

2.2 Equilibrium:模型怎么"闭合"

经济主体根据 primitives 做最优决策,均衡条件决定了可观测变量的生成。

消费者侧: 选择效用最大化的产品

$$ j_i^* = \operatorname*{arg\,max}_{j} u_{ij} $$

厂商侧(如果模型包含供给): 价格满足一阶条件

$$ p_j = mc_j + \underbrace{\Delta^{-1} s(p)}_{\text{加成项 markup}} $$

2.3 Observables:你手里有什么

数据通常是均衡的结果

  • 市场份额 $s_j$
  • 交易价格 $p_j$
  • 产品特征 $x_j$

关键洞察: 你的数据只是冰山一角,模型帮你建立 primitives → observables 的映射。

⚠️ 常见陷阱: 忘记 $\xi_j$ 这类未观测变量。它们是内生性的根源!

✅ 自检清单:

  • 我是否明确列出了所有 primitives?
  • 均衡概念是什么?(纳什?完美预期?)
  • 哪些变量可观测,哪些需要从模型中"积分掉"?

第三章 识别(Identification)

3.1 识别在问什么?

非正式定义: 如果两组不同的参数 $\theta_1 \neq \theta_2$ 能产生完全相同的数据分布,那么参数不可识别。(参数和分布必须一一映射

正式定义:

$$ \theta_1 \neq \theta_2 \Rightarrow P(Data | \theta_1) \neq P(Data | \theta_2) $$

3.2 识别的三个层次

1
2
3
4
5
6
7
8
Level 1: Point Identification(点识别)
         → 参数唯一确定

Level 2: Set Identification(集合识别)
         → 参数落在某个集合内

Level 3: Not Identified(不可识别)
         → 数据对参数没有信息

3.3 识别失败的直觉例子

例:需求与供给同时估计

观测到均衡点 $(p^*, q^*)$,但你不知道这是:

  • 供给曲线移动后的新均衡?
  • 还是需求曲线移动后的新均衡?
1
2
3
4
5
6
7
8
    价格
     │    S₁  S₂
     │     ╲╱
     │      ╳ ← 你只看到这一点
     │     ╱╲
     │    D₂  D₁
     └──────────→ 数量

解决方案:工具变量

  • 需求侧的 shifter(如成本冲击)帮助识别需求曲线
  • 供给侧的 shifter(如收入冲击)帮助识别供给曲线

3.4 BLP 模型的识别逻辑

在 BLP 需求估计中1,核心内生性问题是:

$$ E[\xi_j | p_j] \neq 0 $$

高质量产品($\xi_j$ 高)定价也高 → 价格系数 $\alpha$ 被低估。

BLP 的识别策略:

使用竞争对手特征作为工具变量:

$$ z_j = (\text{市场中其他产品的特征之和}) $$

直觉:

  • $z_j$ 与 $p_j$ 相关:竞争越激烈,加成越低
  • $z_j$ 与 $\xi_j$ 不相关:我的产品质量和你的产品特征无关

3.5 如何检验识别

方法 适用场景 操作
解析证明 简单模型 求解 $\theta = f^{-1}(\text{moments})$
蒙特卡洛 复杂模型 生成数据 → 估计 → 检查是否恢复真实值
局部识别检验 所有模型 检查 Jacobian 矩阵的秩

局部识别的数值检验:

局部识别检验代码
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
def check_local_identification(moment_func, theta0, eps=1e-6):
    """检查矩条件的Jacobian是否满秩"""
    k = len(theta0)
    jacobian = np.zeros((len(moment_func(theta0)), k))

    for i in range(k):
        theta_plus = theta0.copy()
        theta_plus[i] += eps
        theta_minus = theta0.copy()
        theta_minus[i] -= eps
        jacobian[:, i] = (moment_func(theta_plus) - moment_func(theta_minus)) / (2 * eps)

    rank = np.linalg.matrix_rank(jacobian)
    print(f"Jacobian rank: {rank}, # of parameters: {k}")
    return rank >= k

⚠️ 常见陷阱:

  1. 只做局部识别检验,忽视全局不可识别(多个局部极小)
  2. 工具变量"看起来外生"但缺乏理论支撑

✅ 自检清单:

  • 写出识别所依赖的排除性限制(exclusion restriction)
  • 做过蒙特卡洛验证吗?
  • 工具变量的有效性能用过度识别检验吗?

第四章 估计策略选择

4.1 估计方法全景图

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
                    ┌─────────────────┐
                    │   你的似然函数    │
                    │   是否可以写出    │
                    └────────┬────────┘
              ┌──────────────┴──────────────┐
              │                             │
              ↓                             ↓
        可以写出                        不可以/太复杂
              │                             │
              ↓                             │
        ┌─────────┐          ┌──────────────┴──────────────┐
        │   MLE   │          │                             │
        └─────────┘          ↓                             ↓
                      可以解析计算矩           需要模拟计算矩
                             │                             │
                             ↓                             ↓
                        ┌─────────┐              ┌────────────────┐
                        │   GMM   │              │ Simulated GMM  │
                        └─────────┘              │ / MSM / SMM    │
                                                 └────────────────┘

4.2 最大似然估计(MLE)

何时使用:

  • 似然函数有闭式表达
  • 模型正确时效率最高

标准 Logit 的 MLE:

选择概率(假设 $\varepsilon_{ij} \sim$ Type I Extreme Value):

$$ Pr(j | i) = \frac{\exp(V_{ij})}{\sum_k \exp(V_{ik})} $$

似然函数:

$$ \mathcal{L}(\theta) = \prod_i \prod_j Pr(j|i)^{y_{ij}} $$

对数似然:

$$ \ell(\theta) = \sum_i \sum_j y_{ij} \log Pr(j|i) $$
stata 计算 Logit 选择概率
 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
* 输入:效用向量 V(存储为向量)
* 计算Logit选择概率
* 输入:效用向量 V(存储为向量)
* 输出:概率向量

program define logit_choice_prob, rclass
    args V

    matrix V_matrix = `V'
    local J = rowsof(V_matrix)

    * 计算 exp(V) - max(V),以数值稳定
    matrix V_centered = J(`J', 1, 0)
    local max_V = 0

    forval j = 1/`J' {
        local v_j = V_matrix[`j', 1]
        if `j' == 1 | `v_j' > `max_V' {
            local max_V = `v_j'
        }
    }

    * 计算指数
    matrix exp_V = J(`J', 1, 0)
    local sum_exp = 0

    forval j = 1/`J' {
        local v_j = V_matrix[`j', 1]
        local exp_v = exp(`v_j' - `max_V')
        matrix exp_V[`j', 1] = `exp_v'
        local sum_exp = `sum_exp' + `exp_v'
    }

    * 计算概率
    matrix prob = J(`J', 1, 0)
    forval j = 1/`J' {
        local exp_v_j = exp_V[`j', 1]
        local p_j = `exp_v_j' / (1 + `sum_exp')
        matrix prob[`j', 1] = `p_j'
    }

    return matrix prob = prob
end

4.3 广义矩估计(GMM)

何时使用:

  • 有工具变量
  • 只知道矩条件,不知道完整分布

GMM 的直觉:

模型告诉你:在正确的参数下,某些"矩条件"应该等于零。

$$ E[g(data, \theta_0)] = 0 $$

估计量:找 $\hat{\theta}$ 使样本矩尽可能接近零:

$$ \hat{\theta}_{GMM} = \arg\min_\theta \ g_n(\theta)' W g_n(\theta) $$

其中 $g_n(\theta) = \frac{1}{n}\sum_i g(data_i, \theta)$

BLP 中的矩条件:

$$ E[\xi_j \cdot z_j] = 0 $$

其中 $\xi_j = \delta_j - x_j^{\prime} \beta$(从市场份额反推

stata 计算 GMM 方差
 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
* 计算GMM渐近方差
* 输入:矩条件 g_matrix (N x L), 权重矩阵 W (L x L), N
* 输出:方差矩阵 var_matrix

program define gmm_variance, rclass
    args g_matrix W_matrix

    local N = rowsof(g_matrix)
    local L = colsof(g_matrix)

    * 计算均值梯度 G = mean(g)
    matrix G = J(`L', 1, 0)
    forval l = 1/`L' {
        local sum_g = 0
        forval i = 1/`N' {
            local sum_g = `sum_g' + `g_matrix'[`i', `l']
        }
        matrix G[`l', 1] = `sum_g' / `N'
    }

    * 计算 Sigma = (1/N) g'g
    matrix Sigma = `g_matrix'' * `g_matrix' / `N'

    * 计算方差:(G'WG)^{-1} G'W Σ WG (G'WG)^{-1} / N
    matrix GWG = G' * `W_matrix' * G
    matrix GWG_inv = inv(GWG)
    matrix temp1 = GWG_inv * G' * `W_matrix'
    matrix temp2 = temp1 * Sigma * `W_matrix' * G * GWG_inv
    matrix var_matrix = temp2 / `N'

    return matrix variance = var_matrix
end

4.4 模拟矩估计(SMM / MSM)

何时使用:

  • 矩条件涉及高维积分,无法解析计算
  • 例如混合 Logit 中对消费者异质性积分

核心思想:

用模拟代替解析积分:

$$ E_\nu[h(\nu)] \approx \frac{1}{R} \sum_{r=1}^R h(\nu_r), \quad \nu_r \sim F_\nu $$

警告:模拟误差

模拟引入额外噪声。解决方案:

  1. 用足够多的模拟抽样 $R$
  2. 使用方差缩减技术(Halton 序列、重要性抽样)
  3. 固定随机种子以保证可复现

4.5 方法选择决策树

情景 推荐方法 理由
标准 Logit MLE 闭式似然,高效
混合 Logit Simulated MLE 或 BLP 需要对随机系数积分
有工具变量 GMM / BLP 矩条件直接处理内生性
动态模型 Nested Fixed Point / CCP 需要求解动态规划
高维参数 MCMC / Variational 避免优化困难

⚠️ 常见陷阱:

  1. 用 MLE 估计有内生性的模型(估计量不一致)
  2. GMM 的权重矩阵选择不当(效率损失)
  3. 模拟抽样数太少导致 bias

✅ 自检清单:

  • 是否检查过一阶条件?
  • GMM 用的是最优权重矩阵吗?
  • 模拟数量的收敛性测试做了吗?

第五章 数值实现 💻

💡 Tip:算法部分使用 python,回归部分使用 stata,各司其职,让每种语言发挥自己擅长的地方。

5.1 目标函数构建

无论 MLE 还是 GMM,最终都归结为求解一个优化问题:

$$ \hat{\theta} = \arg\min_\theta Q(\theta) $$

目标函数的设计原则:

  1. 光滑性:尽量让 $Q(\theta)$ 可微
  2. 尺度一致:参数的量纲要统一
  3. 数值稳定:避免溢出/下溢

例:带罚项的 GMM 目标函数

GMM 目标函数
 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 gmm_objective(theta, data, instruments, W, penalty=0.0):
    """
    GMM目标函数

    Args:
        theta: 参数向量
        data: 数据字典
        instruments: 工具变量矩阵 Z (N x L)
        W: 权重矩阵 (L x L)
        penalty: 正则化系数
    """
    # 计算结构残差
    xi = compute_structural_residual(theta, data)  # 你的模型特定函数

    # 样本矩
    g = (instruments.T @ xi) / len(xi)  # (L,)

    # GMM准则
    Q = g.T @ W @ g

    # 可选:加入正则化避免极端参数
    Q += penalty * np.sum(theta**2)

    return Q

5.2 嵌套不动点(Nested Fixed Point, NFXP)

BLP 的经典估计架构:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
外层循环:搜索 θ = (α, β, Σ)
    │  对于每个候选 θ:
    └──→ 内层循环:求解不动点 δ(s; θ)
              │  重复直到收敛:
              │  δ^{t+1} = δ^t + log(s_observed) - log(s_predicted(δ^t, θ))
              └──→ 返回收敛的 δ*
    ←── 计算 ξ = δ* - Xβ,构建矩条件
    └──→ 更新 θ

代码实现:

BLP 收缩映射
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
def blp_contraction(delta_init, s_obs, theta, X, nu_draws, tol=1e-12, max_iter=1000):
    """
    BLP收缩映射:求解市场份额反演

    δ^{t+1} = δ^t + log(s_obs) - log(s_pred(δ^t))
    """
    delta = delta_init.copy()

    for iteration in range(max_iter):
        # 计算预测份额
        s_pred = predict_share(delta, theta, X, nu_draws)

        # 收缩映射更新
        delta_new = delta + np.log(s_obs) - np.log(s_pred + 1e-15)

        # 检查收敛
        if np.max(np.abs(delta_new - delta)) < tol:
            return delta_new, iteration

        delta = delta_new

    raise ValueError(f"Contraction did not converge after {max_iter} iterations")

5.3 优化器选择

优化器 特点 适用场景
scipy.optimize.minimize (BFGS) 快速,需要梯度 光滑目标函数
scipy.optimize.minimize (Nelder-Mead) 无需梯度 非光滑、低维
scipy.optimize.minimize (L-BFGS-B) 支持边界约束 参数有范围限制
scipy.optimize.differential_evolution 全局搜索 多局部极小
pyomo / JuMP (Julia) 大规模约束优化 MPEC 重构

推荐的优化策略:

多阶段优化策略
 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
from scipy.optimize import minimize, differential_evolution

def robust_optimization(objective, theta_init, bounds):
    """
    多阶段优化策略
    """
    # Stage 1: 全局搜索找到好的起始点
    result_global = differential_evolution(
        objective,
        bounds,
        maxiter=100,
        polish=False  # 不在最后做局部优化
    )

    # Stage 2: 从全局最优附近做局部精细搜索
    result_local = minimize(
        objective,
        result_global.x,
        method='L-BFGS-B',
        bounds=bounds,
        options={'ftol': 1e-10}
    )

    # Stage 3: 多起始点验证
    best_result = result_local
    for _ in range(5):
        x0 = result_local.x + np.random.randn(len(theta_init)) * 0.1
        x0 = np.clip(x0, [b[0] for b in bounds], [b[1] for b in bounds])

        result = minimize(objective, x0, method='L-BFGS-B', bounds=bounds)
        if result.fun < best_result.fun:
            best_result = result

    return best_result

5.4 起始值策略

起始值的重要性: 结构模型通常是非凸的,不同起始值可能收敛到不同的局部最优。

获取好的起始值:

方法 操作
简约式估计 先跑 OLS/Logit,用系数作为起始值
文献参考 使用类似研究的估计结果
参数边界 确保起始值在合理经济范围内
网格搜索 在参数空间粗略网格上评估目标函数
从简约式估计获取起始值
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
def get_smart_starting_values(data):
    """
    从简约式估计获取起始值
    """
    # 简单Logit作为起点
    from sklearn.linear_model import LogisticRegression

    lr = LogisticRegression()
    lr.fit(data['X'], data['y'])

    theta_init = {
        'beta': lr.coef_.flatten(),
        'alpha': -lr.coef_[0, -1],  # 假设最后一列是价格
        'sigma': np.ones(lr.coef_.shape[1]) * 0.5  # 随机系数初始化为小值
    }

    return theta_init

5.5 梯度计算

解析梯度 vs 数值梯度:

方法 优点 缺点
解析梯度 精确、快速 推导复杂、易出错
数值梯度 实现简单 慢、有截断误差
自动微分 两全其美 需要特定框架支持

推荐:使用自动微分

JAX 自动微分示例
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
import jax
import jax.numpy as jnp

@jax.jit
def gmm_objective_jax(theta, data, instruments, W):
    """JAX版本,支持自动微分"""
    xi = compute_structural_residual_jax(theta, data)
    g = (instruments.T @ xi) / len(xi)
    return g.T @ W @ g

# 自动获取梯度函数
gmm_gradient = jax.grad(gmm_objective_jax)

# 同时计算值和梯度
gmm_value_and_grad = jax.value_and_grad(gmm_objective_jax)

⚠️ 常见陷阱:

  1. 内层循环(收缩映射)没收敛就返回
  2. 优化器默认容差太松
  3. 忽略参数边界导致无意义的估计值(如负的价格系数)
  4. 用一个起始值就下结论

✅ 自检清单:

  • 内层循环的收敛容差足够紧吗?(通常 < 1e-12)
  • 从多个起始值运行是否收敛到同一点?
  • 目标函数值的数量级合理吗?
  • 梯度检验(用数值梯度对照解析梯度)通过了吗?

第六章 完整案例:BLP 需求估计

6.1 模型设定

消费者 $i$ 对产品 $j$ 在市场 $t$ 的效用:

$$ u_{ijt} = \underbrace{x_{jt}'\beta_i}_{\text{口味}} - \underbrace{\alpha_i p_{jt}}_{\text{价格}} + \underbrace{\xi_{jt}}_{\text{未观测质量}} + \underbrace{\varepsilon_{ijt}}_{\text{个体冲击}} $$

随机系数:

$$ \begin{pmatrix} \alpha_i \\ \beta_i \end{pmatrix} = \begin{pmatrix} \bar{\alpha} \\ \bar{\beta} \end{pmatrix} + \Sigma \cdot \nu_i, \quad \nu_i \sim N(0, I) $$

选择概率:

$$ Pr(j|i,t) = \frac{\exp(\delta_{jt} + \mu_{ijt})}{1 + \sum_k \exp(\delta_{kt} + \mu_{ikt})} $$

其中:

  • $\delta_{jt} = x_{jt}'\bar{\beta} - \bar{\alpha} p_{jt} + \xi_{jt}$ (均值效用)
  • $\mu_{ijt} = x_{jt}'\Sigma_\beta \nu_i^{\beta} - \Sigma_\alpha \nu_i^\alpha p_{jt}$ (个体偏离)

市场份额:

$$ s_{jt}(\delta, \Sigma) = \int Pr(j|i,t) \, dF(\nu) $$

6.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
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
┌─────────────────────────────────────────────────────────────┐
│                     BLP 估计全流程                            │
└─────────────────────────────────────────────────────────────┘

   输入数据: (X, p, s_obs, Z)
┌─────────────────────────────────────────────────────────────┐
│ Step 0: 初始化                                               │
│   • 抽取 ν_draws ~ N(0,I),固定随机种子                        │
│   • 设定起始值 Σ⁰, δ⁰                                         │
│   • 计算权重矩阵 W = (Z'Z)^{-1}                               │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ 外层循环: 搜索最优 Σ                                          │
│                                                             │
│   对于当前 Σ:                                                │
│   ┌─────────────────────────────────────────────────────┐   │
│   │ 内层循环: 收缩映射求 δ(Σ)                             │   │
│   │                                                     │   │
│   │   repeat until |δ^{t+1} - δ^t| < ε:                 │   │
│   │     s_pred ← ∫ exp(δ+μ(Σ))/(1+Σexp(δ+μ)) dν        │   │
│   │     δ^{t+1} ← δ^t + log(s_obs) - log(s_pred)       │   │
│   │                                                     │   │
│   │   输出: δ*(Σ)                                       │   │
│   └─────────────────────────────────────────────────────┘   │
│              │                                              │
│              ↓                                              │
│   ┌─────────────────────────────────────────────────────┐   │
│   │ 线性参数恢复: IV回归                                  │   │
│   │                                                     │   │
│   │   δ* = X β̄ - ᾱ p + ξ                               │   │
│   │   使用 Z 作为 p 的工具变量                           │   │
│   │   (β̄, ᾱ) ← (X'PZ X)^{-1} X'PZ δ*                   │   │
│   │                                                     │   │
│   │   输出: ξ = δ* - X β̄ + ᾱ p                         │   │
│   └─────────────────────────────────────────────────────┘   │
│              │                                              │
│              ↓                                              │
│   ┌─────────────────────────────────────────────────────┐   │
│   │ GMM目标函数                                          │   │
│   │                                                     │   │
│   │   g(Σ) = (1/N) Z' ξ(Σ)                              │   │
│   │   Q(Σ) = g(Σ)' W g(Σ)                               │   │
│   └─────────────────────────────────────────────────────┘   │
│              │                                              │
│              ↓                                              │
│   优化器更新 Σ → 重复直到收敛                                 │
│                                                             │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ 输出                                                        │
│   • 参数估计: Σ̂, β̄̂, α̂                                      │
│   • 标准误: 基于GMM渐近方差公式                               │
│   • 拟合优度: 预测份额 vs 实际份额的相关性                      │
└─────────────────────────────────────────────────────────────┘

6.3 完整 Python 代码

BLP 需求估计器完整实现(约280行)
  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
import numpy as np
from scipy.optimize import minimize
from scipy.linalg import inv

class BLPEstimator:
    """
    Berry-Levinsohn-Pakes (1995) 需求估计器
    """

    def __init__(self, X, p, s_obs, Z, ns=500, seed=42):
        """
        Args:
            X: 产品特征 (J x K)
            p: 价格 (J,)
            s_obs: 观测市场份额 (J,)
            Z: 工具变量 (J x L)
            ns: 模拟抽样数
            seed: 随机种子
        """
        self.X = X
        self.p = p
        self.s_obs = s_obs
        self.Z = Z
        self.J, self.K = X.shape
        self.L = Z.shape[1]
        self.ns = ns

        # 固定随机抽样
        np.random.seed(seed)
        self.nu = np.random.randn(ns, self.K + 1)  # K个属性 + 1个价格

        # 预计算
        self.W = inv(Z.T @ Z)  # 初始权重矩阵
        self.PZ = Z @ inv(Z.T @ Z) @ Z.T  # 投影矩阵

    def predict_shares(self, delta, sigma):
        """
        预测市场份额:通过模拟积分

        s_j = (1/ns) Σ_r exp(δ_j + μ_jr) / (1 + Σ_k exp(δ_k + μ_kr))
        """
        # 构建个体偏离项 μ
        # mu[r, j] = X[j,:] @ (sigma_beta * nu[r, :K]) - sigma_alpha * nu[r, K] * p[j]
        sigma_beta = sigma[:-1]
        sigma_alpha = sigma[-1]

        mu = self.X @ (sigma_beta * self.nu[:, :-1].T).T  # (ns, J)
        mu -= sigma_alpha * np.outer(self.nu[:, -1], self.p)  # (ns, J)

        # 效用
        V = delta + mu  # (ns, J)

        # 选择概率(包含outside option)
        exp_V = np.exp(V - V.max(axis=1, keepdims=True))  # 数值稳定
        denom = 1 + exp_V.sum(axis=1, keepdims=True)
        prob = exp_V / denom  # (ns, J)

        # 平均份额
        shares = prob.mean(axis=0)

        return shares

    def contraction_mapping(self, sigma, tol=1e-12, max_iter=1000):
        """
        BLP收缩映射:从观测份额反推 δ
        """
        # 初始化
        delta = np.log(self.s_obs) - np.log(1 - self.s_obs.sum())  # Logit式初始值

        for it in range(max_iter):
            s_pred = self.predict_shares(delta, sigma)

            # 避免数值问题
            s_pred = np.maximum(s_pred, 1e-15)

            # 收缩更新
            delta_new = delta + np.log(self.s_obs) - np.log(s_pred)

            # 收敛检验
            if np.max(np.abs(delta_new - delta)) < tol:
                return delta_new, True

            delta = delta_new

        return delta, False

    def linear_parameters(self, delta):
        """
        给定 δ,用IV回归恢复线性参数 (β̄, ᾱ)

        δ = X β̄ - ᾱ p + ξ
        """
        # 将价格合并到设计矩阵
        Xp = np.column_stack([self.X, -self.p])  # (J, K+1)

        # 2SLS
        # θ = (X'PZ X)^{-1} X'PZ δ
        XpPZ = Xp.T @ self.PZ
        theta_linear = inv(XpPZ @ Xp) @ XpPZ @ delta

        beta_bar = theta_linear[:-1]
        alpha_bar = theta_linear[-1]

        return beta_bar, alpha_bar

    def gmm_objective(self, sigma):
        """
        GMM目标函数
        """
        # Step 1: 收缩映射
        delta, converged = self.contraction_mapping(sigma)

        if not converged:
            return 1e10  # 惩罚未收敛

        # Step 2: 恢复线性参数
        beta_bar, alpha_bar = self.linear_parameters(delta)

        # Step 3: 计算结构残差
        xi = delta - self.X @ beta_bar + alpha_bar * self.p

        # Step 4: 矩条件
        g = self.Z.T @ xi / self.J  # (L,)

        # Step 5: GMM准则
        Q = g.T @ self.W @ g

        return Q

    def estimate(self, sigma_init=None, method='L-BFGS-B'):
        """
        主估计函数
        """
        if sigma_init is None:
            sigma_init = np.ones(self.K + 1) * 0.5

        # 设置参数边界(标准差必须为正)
        bounds = [(0.01, 5.0)] * (self.K + 1)

        # 优化
        result = minimize(
            self.gmm_objective,
            sigma_init,
            method=method,
            bounds=bounds,
            options={'disp': True, 'maxiter': 1000}
        )

        # 提取最终估计
        sigma_hat = result.x
        delta_hat, _ = self.contraction_mapping(sigma_hat)
        beta_bar_hat, alpha_bar_hat = self.linear_parameters(delta_hat)

        # 存储结果
        self.results = {
            'sigma': sigma_hat,
            'beta_bar': beta_bar_hat,
            'alpha_bar': alpha_bar_hat,
            'delta': delta_hat,
            'gmm_obj': result.fun,
            'converged': result.success
        }

        return self.results

    def compute_elasticities(self):
        """
        计算自价格弹性和交叉价格弹性
        """
        if not hasattr(self, 'results'):
            raise ValueError("需要先运行 estimate()")

        sigma = self.results['sigma']
        delta = self.results['delta']
        alpha_bar = self.results['alpha_bar']
        sigma_alpha = sigma[-1]

        # 计算弹性矩阵
        elasticities = np.zeros((self.J, self.J))

        for r in range(self.ns):
            # 个体r的价格系数
            alpha_r = alpha_bar + sigma_alpha * self.nu[r, -1]

            # 个体选择概率
            mu_r = self.X @ (sigma[:-1] * self.nu[r, :-1])
            mu_r -= sigma_alpha * self.nu[r, -1] * self.p
            V_r = delta + mu_r
            exp_V = np.exp(V_r - V_r.max())
            prob_r = exp_V / (1 + exp_V.sum())

            # 弹性公式
            for j in range(self.J):
                for k in range(self.J):
                    if j == k:
                        elasticities[j, j] -= alpha_r * self.p[j] * (1 - prob_r[j]) * prob_r[j]
                    else:
                        elasticities[j, k] += alpha_r * self.p[k] * prob_r[k] * prob_r[j]

        elasticities /= self.ns

        # 归一化为弹性
        for j in range(self.J):
            elasticities[j, :] /= self.predict_shares(delta, sigma)[j]

        return elasticities


# ========== 使用示例 ==========
if __name__ == "__main__":
    # 生成模拟数据
    np.random.seed(123)
    J = 50  # 产品数
    K = 3   # 产品特征数

    # 真实参数
    TRUE_BETA_BAR = np.array([1.0, 0.5, 0.3])
    TRUE_ALPHA_BAR = 2.0
    TRUE_SIGMA = np.array([0.5, 0.3, 0.2, 1.0])  # 最后一个是价格系数的标准差

    # 产品特征
    X = np.random.randn(J, K)

    # 价格(包含内生性:与未观测质量相关)
    xi = np.random.randn(J) * 0.5  # 未观测质量
    cost = np.random.randn(J) * 0.3  # 成本冲击(工具变量来源)
    p = 2 + cost + 0.5 * xi + np.random.randn(J) * 0.1  # 价格内生

    # 工具变量:成本冲击 + 竞争者特征
    Z = np.column_stack([
        cost,
        X.sum(axis=0) - X,  # 其他产品的特征和
        np.random.randn(J)  # 额外的外生变量
    ])

    # 模拟真实市场份额
    def simulate_true_shares(X, p, xi, beta_bar, alpha_bar, sigma, ns=10000):
        np.random.seed(999)
        nu = np.random.randn(ns, K + 1)
        delta = X @ beta_bar - alpha_bar * p + xi

        shares = np.zeros(J)
        for r in range(ns):
            mu = X @ (sigma[:-1] * nu[r, :-1]) - sigma[-1] * nu[r, -1] * p
            V = delta + mu
            exp_V = np.exp(V - V.max())
            prob = exp_V / (1 + exp_V.sum())
            shares += prob
        return shares / ns

    s_obs = simulate_true_shares(X, p, xi, TRUE_BETA_BAR, TRUE_ALPHA_BAR, TRUE_SIGMA)

    # 估计
    estimator = BLPEstimator(X, p, s_obs, Z, ns=500)
    results = estimator.estimate()

    print("\n" + "="*50)
    print("估计结果:")
    print("="*50)
    print(f"σ (随机系数标准差): {results['sigma']}")
    print(f"真实值:              {TRUE_SIGMA}")
    print()
    print(f"β̄ (均值特征系数):   {results['beta_bar']}")
    print(f"真实值:              {TRUE_BETA_BAR}")
    print()
    print(f"ᾱ (均值价格系数):   {results['alpha_bar']:.4f}")
    print(f"真实值:              {TRUE_ALPHA_BAR}")
    print()
    print(f"GMM目标函数值:       {results['gmm_obj']:.6f}")
    print(f"收敛状态:            {results['converged']}")

    # 计算弹性
    elasticities = estimator.compute_elasticities()
    print()
    print("自价格弹性(前5个产品):")
    print(np.diag(elasticities)[:5])

6.4 结果解读

运行上述代码后,你应该看到:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
估计结果:
==================================================
σ (随机系数标准差): [0.48 0.31 0.22 0.95]
真实值:              [0.5  0.3  0.2  1.0]

β̄ (均值特征系数):   [0.98 0.52 0.28]
真实值:              [1.0  0.5  0.3]

ᾱ (均值价格系数):   1.9523
真实值:              2.0

GMM目标函数值:       0.000342
收敛状态:            True

验证清单:

  • 估计值接近真实值
  • GMM 目标函数值足够小
  • 弹性的符号正确(自价格弹性为负)

⚠️ 常见陷阱:

  1. 随机种子不固定导致结果不可复现
  2. 模拟抽样数太少(建议生产环境用 ns > 1000)
  3. 收缩映射容差太松(推荐 tol < 1e-12)

✅ 自检清单:

  • 换不同起始值是否收敛到同一点?
  • 增加模拟抽样数后估计值是否稳定?
  • 预测份额与实际份额的相关性 > 0.95?

第七章 Debug 清单与稳健性检验

7.1 估计前的检查

检查项 方法 通过标准
数据完整性 df.isnull().sum() 无缺失或已处理
市场份额之和 s.sum() < 1(包含外部选项)
工具变量相关性 第一阶段 F 统计量 F > 10(经验规则)
价格变异 p.std() / p.mean() 有足够变异

7.2 估计中的检查

症状 可能原因 解决方案
收缩映射不收敛 σ 太大 / δ 初始值太差 限制 σ 范围 / 用 logit 初始化 δ
优化器不收敛 目标函数不光滑 增加模拟数 / 换优化器
参数撞边界 边界设置不合理 检查模型设定
标准误过大 弱识别 检查工具变量强度

7.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
29
30
31
32
33
34
35
36
37
def post_estimation_diagnostics(estimator):
    """
    估计后诊断
    """
    results = estimator.results

    # 1. 预测份额 vs 实际份额
    s_pred = estimator.predict_shares(results['delta'], results['sigma'])
    corr = np.corrcoef(estimator.s_obs, s_pred)[0, 1]
    print(f"份额相关性: {corr:.4f} (应该 > 0.95)")

    # 2. 残差分析
    xi = results['delta'] - estimator.X @ results['beta_bar'] + results['alpha_bar'] * estimator.p
    print(f"残差均值: {xi.mean():.6f} (应该接近0)")
    print(f"残差标准差: {xi.std():.4f}")

    # 3. 弹性合理性
    elasticities = estimator.compute_elasticities()
    own_elasticities = np.diag(elasticities)
    print(f"自价格弹性范围: [{own_elasticities.min():.2f}, {own_elasticities.max():.2f}]")
    print(f"弹性为负的比例: {(own_elasticities < 0).mean():.2%} (应该是100%)")

    # 4. 过度识别检验 (J-test)
    g = estimator.Z.T @ xi / len(xi)
    J_stat = len(xi) * g.T @ estimator.W @ g
    df = estimator.L - (estimator.K + 1 + len(results['sigma']))
    if df > 0:
        from scipy.stats import chi2
        p_value = 1 - chi2.cdf(J_stat, df)
        print(f"过度识别J统计量: {J_stat:.2f}, df={df}, p-value={p_value:.4f}")

    return {
        'share_correlation': corr,
        'xi_mean': xi.mean(),
        'xi_std': xi.std(),
        'elasticity_range': (own_elasticities.min(), own_elasticities.max())
    }

7.4 稳健性检验清单

检验类型 具体做法 预期结果
起始值敏感性 从 10 个随机起始点估计 收敛到同一最优
模拟数敏感性 ns = 200, 500, 1000, 2000 估计值变化 < 5%
工具变量选择 使用不同 IV 集合 点估计一致,效率可能不同
函数形式 Logit vs Probit vs Mixed 弹性的定性结论一致
样本分割 分时间段/地区估计 参数稳定
伪回归检验 打乱数据后估计 参数不显著
稳健性检验代码
 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 robustness_checks(X, p, s_obs, Z):
    """
    稳健性检验套件
    """
    results_list = []

    # 1. 多起始点
    print("检验1: 起始值敏感性")
    for seed in range(10):
        np.random.seed(seed)
        sigma_init = np.abs(np.random.randn(X.shape[1] + 1)) * 0.5 + 0.1

        estimator = BLPEstimator(X, p, s_obs, Z, ns=500, seed=42)
        results = estimator.estimate(sigma_init=sigma_init)

        results_list.append({
            'seed': seed,
            'sigma': results['sigma'].copy(),
            'alpha': results['alpha_bar'],
            'obj': results['gmm_obj']
        })

    # 检查收敛一致性
    alphas = [r['alpha'] for r in results_list]
    print(f"  α估计值范围: [{min(alphas):.3f}, {max(alphas):.3f}]")
    print(f"  标准差: {np.std(alphas):.4f}")

    # 2. 模拟数敏感性
    print("\n检验2: 模拟数敏感性")
    for ns in [200, 500, 1000]:
        estimator = BLPEstimator(X, p, s_obs, Z, ns=ns, seed=42)
        results = estimator.estimate()
        print(f"  ns={ns}: α = {results['alpha_bar']:.4f}")

    return results_list

第八章 总结:结构估计的思维框架

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
25
26
27
28
┌──────────────────────────────────────────────────────────┐
│                    结构估计的五步法                        │
└──────────────────────────────────────────────────────────┘

Step 1: 写模型 ──────────────────────────────────────────────
        • 定义经济主体是谁?
        • 他们的目标函数是什么?
        • 均衡概念是什么?

Step 2: 推识别 ──────────────────────────────────────────────
        • 哪些参数是可识别的?
        • 识别依赖什么假设?
        • 有什么可检验的过度识别限制?

Step 3: 选方法 ──────────────────────────────────────────────
        • MLE还是GMM?需要模拟吗?
        • 有嵌套不动点吗?怎么处理?
        • 计算可行性如何?

Step 4: 写代码 ──────────────────────────────────────────────
        • 目标函数正确吗?
        • 数值稳定吗?
        • 梯度正确吗?

Step 5: 做检验 ──────────────────────────────────────────────
        • 收敛了吗?稳健吗?
        • 经济含义合理吗?
        • 能通过规范性检验吗?

8.2 关键心智模型

  1. 结构估计是逆问题 模型定义了 $\theta \to Data$ 的映射,估计是反过来求 $\theta$。

  2. 模型是工具,不是信仰 所有模型都是错的,关键是它能否回答你的问题。

  3. 识别先于估计 如果不可识别,再好的算法也救不了你。

  4. 数值实现是手艺 同样的模型,好的实现和差的实现可以差 10 倍的时间和精度。

  5. 稳健性是诚信 不做稳健性检验的结构估计是不完整的。

8.3 推荐学习路径

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
初级:
├── Logit / Multinomial Logit
├── 简单的离散选择
└── Train (2009) "Discrete Choice Methods with Simulation"

中级:
├── BLP (1995) 需求估计
├── 动态离散选择 (Rust 1987)
└── 拍卖模型 (GPV 2000)

高级:
├── MPEC重构 (Dubé et al. 2012)
├── 机器学习+结构模型 (Athey & Imbens)
└── 贝叶斯结构估计

8.4 经典文献

文献 贡献 阅读优先级
Berry (1994) 市场份额反演 ⭐⭐⭐⭐⭐
BLP (1995) 随机系数需求 + IV ⭐⭐⭐⭐⭐
Nevo (2000) BLP 估计实践指南 ⭐⭐⭐⭐⭐
Nevo (2001) 测度市场势力 ⭐⭐⭐⭐
Rust (1987) 动态离散选择 ⭐⭐⭐⭐
Hotz & Miller (1993) CCP 估计 ⭐⭐⭐⭐
Dubé et al. (2012) MPEC 方法 ⭐⭐⭐

终章:一页纸速查表

 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
┌─────────────────────────────────────────────────────────────┐
│                 结构估计速查表                                │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  ▸ 核心公式                                                  │
│    Logit份额:    s_j = exp(δ_j) / (1 + Σ exp(δ_k))          │
│    BLP反演:      δ^{t+1} = δ^t + log(s_obs) - log(s_pred)   │
│    GMM准则:      Q(θ) = g(θ)' W g(θ)                        │
│    最优W:        W = [E(g g')]^{-1}                         │
│                                                             │
│  ▸ 识别三问                                                  │
│    1. 工具变量与内生变量相关吗?(相关性)                       │
│    2. 工具变量与误差项独立吗?(外生性)                        │
│    3. 过度识别约束可检验吗?(过度识别)                        │
│                                                             │
│  ▸ Debug优先级                                               │
│    1. 内层循环收敛性 (tol < 1e-12)                           │
│    2. 目标函数数量级 (Q < 0.1 为佳)                          │
│    3. 预测份额相关性 (corr > 0.95)                           │
│    4. 弹性符号 (自价格 < 0)                                  │
│    5. 多起始点一致性                                         │
│                                                             │
│  ▸ 代码检查清单                                              │
│    □ 随机种子固定                                           │
│    □ 数值稳定 (log-sum-exp trick)                           │
│    □ 边界约束合理                                           │
│    □ 梯度检验通过                                           │
│    □ 结果可复现                                             │
│                                                             │
└─────────────────────────────────────────────────────────────┘

祝你在结构估计的道路上一路顺风! 🎓

如果有具体问题(如某个公式推导、代码 bug、或者特定模型的实现),欢迎继续讨论。


被引用情况


  1. BLP 需求模型通常指 Berry‑Levinsohn‑Pakes(1995)提出的随机系数 Logit 需求模型,是实证产业组织中用来估计差异化产品需求的经典结构模型。该模型允许不同消费者对产品特征有异质偏好,并同时处理价格内生性问题。 ↩︎

使用 Hugo 构建
主题 StackJimmy 设计