Bunching / Kink 聚束设计:从拐点处的"扎堆"反推行为弹性
当税制、补贴或监管规则在某个收入(或产量、工资)水平上制造一个预算集的"折点"(kink)或"跳跃"(notch),最优化的个体会主动把自己的选择"挤"到折点上,形成直方图上肉眼可见的一个峰。Bunching design 的核心思想是:这个峰有多"鼓",恰恰度量了个体对价格激励的行为弹性。本页讲清 Saez (2010) 的 reduced-form 弹性估计量、kink 与 notch 的区别、反事实密度的多项式拟合、excess mass 的 bootstrap 标准误,以及中文顶刊里个税与小微企业税收优惠的应用。
bunching、histogram、polyfit、bootstrap);Python(numpy、scipy、statsmodels 可选)。01 是什么:bunching、kink 与 notch
Bunching(聚束/群聚)指的是:当政策在某个 running variable 上制造了一个不连续的激励时,大量最优化个体会把自己的选择恰好停在这个不连续点上,从而在该变量的分布直方图上形成一个肉眼可见的"鼓包"。最经典的例子是个人所得税的税率跳档:中国 2011 年税改后,月应纳税所得额在 3500 元(免征额)以上的部分适用第一档 3% 税率,越过 1500 元(即应纳税所得额 5000 元)后税率跳到 10%。一个原本会赚 5100 元的纳税人,理性选择是把工资"做"到 5000 元整——因为多赚 100 元只按 3% 边际税率缴税,而越过门槛后边际税率翻倍,反而不划算。于是 5000 元这个点上"挤"了一大堆人。
需要区分两个概念:
- Kink design(折点设计):预算集的斜率在拐点处变化,即边际税率 $t$ 从 $t_0$ 跳到 $t_1$,但预算线本身连续。个体在拐点处形成一个点质量(point mass),分布在拐点处"鼓"一下,两侧平滑。
- Notch design(断点/缺口设计):预算集在阈值处发生水平位移(例如跨过门槛后一次性多缴一笔固定税,或失去一笔一次性补贴)。此时预算线在阈值处向下跳断,跨过阈值反而可能更穷。理性个体会在阈值右侧留出一个空洞(hole / dominated region)——没有人愿意选择那个区间,因为左端严格占优。
两种设计都能识别弹性,但 notch 多出一个"支配区间"需要额外建模;kink 是 reduced-form 版本,估计更干净。Kleven (2016, JEL) 的综述把这一类方法统称为 "bunching design",与 RDD 的区别在于:RDD 利用的是结果变量在 cutoff 处的跳跃,bunching 利用的是 running variable 自身密度在 cutoff 处的隆起。
02 识别假设与经济学原理
Bunching design 的识别逻辑非常"结构":它假设个体在给定预算集下做最优化,分布由偏好异质性驱动。核心假设包括:
- 优化无摩擦(frictionless optimization):个体能精确地把收入"摆"到拐点上,不存在调整成本、注意力缺失、操纵误差。Saez 2010 的原始模型假设无摩擦;后续 Chetty-Friedman-Olsen-Pistaferri (2011, AER) 引入了优化摩擦(optimization frictions),发现现实中 bunching 峰是"弥散"的。
- 反事实密度平滑(smooth counterfactual density):如果没有这个拐点,收入分布 $h_0(z)$ 在拐点附近应该是平滑的(由多项式近似)。实际密度 $h(z)$ 与反事实密度 $h_0(z)$ 之差就是"被挤过来的人"。
- 局部小弹性 / 局部近似:当税率变化幅度较小时,可以用局部线性近似把"移动距离"和"弹性"联系起来。
- 非策略性报告 / 无操纵:与 RDD 的 rddensity 检验类似,如果 bunching 是因为企业/个人精确操纵发票、账目(而非真实行为调整),弹性估计会被高估。
直觉:拐点处挤进来的人越多(excess mass $B$ 越大),说明个体对边际税率变化越敏感——也就是劳动供给弹性越大。把 $B$ 除以拐点处的"基线密度" $h_0(z^*)$,就得到平均移动距离 $\Delta z^*$;再用税率变化幅度做分母,就把移动距离换算成弹性。
03 完整推导:从效用最大化到 Saez 弹性公式
沿用 Saez (2010) 的准线性劳动供给模型。个体选择劳动收入 $z$,效用函数为:
其中 $c$ 为消费,$z$ 为税前收入,$n$ 为能力异质性参数(分布密度为 $f(n)$),$e$ 为常数弹性(elasticity of taxable income, ETI)。在比例税率 $t$ 下预算约束为 $c = z(1-t)$。一阶条件:
取对数:$\log z^* = \log n + e\log(1-t)$。现在在 $z^*$ 处制造一个 kink:边际税率从 $t_0$ 跳到 $t_1 > t_0$。原来在无 kink 情形下收入为 $z^*+\Delta z^*$ 的"边际个体",在 kink 下发现把收入降到 $z^*$ 更优(因为 $z^*$ 以上的边际回报下降)。他正好停在拐点:
当税率变化较小,$\log(1-t_1) - \log(1-t_0) \approx -\dfrac{t_1-t_0}{1-t_0}$,取对数后线性化:
另一方面,所有原本落在 $[z^*, z^*+\Delta z^*]$ 区间的人都被"挤"到 $z^*$ 点上,这部分人数就是聚束质量(excess mass):
把 Eq. 3.5 代入 Eq. 3.4,解出弹性:
这就是 Saez (2010, AEJ:EP) 公式。用户提示中的简化式 $e=\frac{B/(h_0+k_1)}{t_1-t_0}$ 是 Eq. 3.6 的粗略记忆版:分子是"每单位密度对应的移动距离",分母是税率相对变化率。实际写作时应使用 Eq. 3.6 的标准形式。
Kink vs Notch 的区别
- Kink:预算线连续、斜率变。只产生点质量,分布在拐点右侧没有"洞"。Eq. 3.6 直接可用。
- Notch:预算线跳断。理性个体不会选择 $(z^*, z^*+\Delta z^{*+})$ 这段被支配区间,分布上出现空洞。估计时需要先识别支配区间(dominated region),再把空洞补回来;弹性从"洞的右边界"反推。Kleven & Waseem (2013, QJE) 用巴基斯坦所得税 notch 做了经典应用。
04 逐步操作流程
05 完整 Stata 代码
下面代码分两部分:先用 Stata 官方/社区 bunching 命令跑一遍(适合快速出图),再手写 histogram + 多项式 + excess mass + bootstrap(适合理解原理与自定义)。所有模拟数据由 set seed 生成,可直接运行。
*==============================================================*
* Bunching / Kink 演示:Saez (2010) 弹性估计
* 情境:月收入 z,在 z*=5000 处边际税率从 t0=0.03 跳到 t1=0.10
*==============================================================*
clear all
set more off
set seed 20260911
* --- 首次运行安装 ---
* ssc install bunching, replace // 社区命令,自动做反事实多项式+bootstrap
* ssc install estout, replace
* --- 1. 模拟数据:能力 n ~ LogNormal,常数弹性 e=0.3 ---
* 无 kink 时 z0 = n*(1-t0)^e;有 kink 时,z0>5000 的人被"挤"到 5000
set obs 50000
gen n = exp(rnormal(8.5, 0.6)) // 能力分布
local e_true = 0.3
local t0 = 0.03
local t1 = 0.10
local zstar = 5000
gen z0 = n*(1-`t0')^`e_true' // 无 kink 时的潜在收入
gen z = z0
* 把 z0 落在 [5000, 5000+dz*] 的边际个体挤到拐点
* dz* 由弹性公式反推:dz*/z* = e*(t1-t0)/(1-t0)
local dz = `zstar' * `e_true' * (`t1'-`t0')/(1-`t0')
replace z = `zstar' if z0>=`zstar' & z0<=`zstar'+`dz'
* --- 2. 用社区命令 bunching 跑(推荐主回归)---
* z 是 running variable,a(5000) 指定拐点,nomore= 不截断右侧
bunching z, a(`zstar') ///
polynomial(4) /// 4 阶多项式拟合反事实
required(500) /// excluded window 左侧 500
cf(500) /// excluded window 右侧 500
binwidth(100) /// 每 bin 100 元
start(-2000) /// 图的左界
generate(bunch) /// 输出估计值到 r()
graph(title("Bunching:个税税率跳档处的聚束"))
* --- 3. 手写版本:histogram + 多项式 + excess mass + bootstrap ---
* 分 bin
local binw = 100
local zmin = 2000
local zmax = 10000
local nb = (`zmax'-`zmin')/`binw'
matrix cnt = J(`nb',1,.)
forvalues j=1/`nb' {
local lo = `zmin' + (`j'-1)*`binw'
local hi = `zmin' + `j'*`binw'
quietly count if z>=`lo' & z<`hi'
matrix cnt[`j',1] = r(N)
}
clear
set obs `nb'
gen cnt = .
gen zc = .
forvalues j=1/`nb' {
quietly replace cnt = cnt[`j',1] in `j'
quietly replace zc = `zmin' + (`j'-0.5)*`binw' in `j'
}
gen rel = zc - `zstar' // 相对拐点的位置
* 多项式拟合反事实:排除拐点左右各 500 元
gen excluded = (abs(rel)<=500)
reg cnt c.rel##c.rel##c.rel##c.rel i.excluded
* 解释:四次多项式 + excluded 虚拟变量;
* excluded 虚拟变量的系数和就是该窗口内被"挤掉"的人数
predict cnt_hat
* excess mass = 实际 - 反事实(在 excluded 窗口内)
gen diff = cnt - cnt_hat if excluded==1
sum diff, detail
scalar B = r(sum)
* 拐点处反事实密度
sum cnt_hat if abs(rel)<=`binw'/2
scalar h0 = r(mean)/`binw'
scalar zstar = `zstar'
* Saez 弹性
scalar e_hat = (B/(h0*zstar)) / ((`t1'-`t0')/(1-`t0'))
display "Excess mass B = " B
display "Saez elasticity e_hat = " e_hat " (true = `e_true')"
* 画直方图 + 反事实线
twoway (bar cnt zc, barwidth(80) color(gs12)) ///
(line cnt_hat zc, lwidth(2) lcolor(navy)), ///
xline(`zstar', lpattern(dash) lcolor(maroon)) ///
legend(label(1 实际人数) label(2 反事实密度)) ///
xtitle("月应纳税所得额") ytitle("每 bin 人数") ///
title("Bunching 手写估计:反事实多项式与 excess mass")
*==============================================================*
* Bootstrap 标准误:对个体有放回重抽样,重复全流程
*==============================================================*
* 把上面"模拟数据"段保留为程序,用 bootstrap 调用
cap prog drop bunching_est
prog define bunching_est, rclass
version 16
syntax , [zstar(real 5000) binw(real 100) w(real 500) t0(real .03) t1(real .10)]
tempvar bin rel excl
gen `bin' = floor((z - (`zstar'-3000))/`binw')
* 直接调用 bunching 命令的返回值
bunching z, a(`zstar') polynomial(4) required(`w') cf(`w') binwidth(`binw') ///
start(-2000) quiet
return scalar e = r(e) // 若命令返回弹性,直接取
return scalar b = r(b) // 标准化 excess mass
end
* 运行 bootstrap(500 次,演示用;正式文章建议 1000-2000 次)
bootstrap e=r(e) b=r(b), reps(500) seed(20260911) ///
saving("bunching_boot.dta", replace): bunching_est
estat bootstrap, all
* 输出:e 的偏差、标准误、正态/百分位/BCa 置信区间
* 报告时建议用 BCa 或 percentile 区间,而非正态近似
# ==============================================================
# Bunching 手写估计:模拟数据 + 多项式反事实 + excess mass
# ==============================================================
import numpy as np
import matplotlib.pyplot as plt
from numpy.polynomial import polynomial as P
rng = np.random.default_rng(20260911)
# --- 1. 模拟数据:与 Stata 版本一致 ---
N = 50000
n = np.exp(rng.normal(8.5, 0.6, N))
e_true, t0, t1, zstar = 0.3, 0.03, 0.10, 5000
z0 = n * (1 - t0) ** e_true
dz = zstar * e_true * (t1 - t0) / (1 - t0)
z = z0.copy()
bunch_mask = (z0 >= zstar) & (z0 <= zstar + dz)
z[bunch_mask] = zstar # 把边际个体挤到拐点
# --- 2. 分 bin ---
binw = 100.0
edges = np.arange(2000, 10001, binw)
centers = (edges[:-1] + edges[1:]) / 2
cnt, _ = np.histogram(z, bins=edges)
# --- 3. 多项式拟合反事实(排除拐点 ±500) ---
rel = centers - zstar
excluded = np.abs(rel) <= 500
mask = ~excluded
coef = np.polyfit(rel[mask], cnt[mask], 4)
cnt_hat = np.polyval(coef, rel)
B = (cnt[excluded] - cnt_hat[excluded]).sum()
h0 = cnt_hat[np.abs(rel) <= binw / 2].mean() / binw
e_hat = (B / (h0 * zstar)) / ((t1 - t0) / (1 - t0))
print(f"Excess mass B = {B:.1f}")
print(f"Saez elasticity e_hat = {e_hat:.3f} (true = {e_true})")
# --- 4. 可视化 ---
fig, ax = plt.subplots(figsize=(8, 4.5))
ax.bar(centers, cnt, width=binw*0.9, color='#cccccc', label='实际人数')
ax.plot(centers, cnt_hat, lw=2, color='#1c5d6e', label='反事实密度(4 阶多项式)')
ax.axvline(zstar, ls='--', color='#a03a2c', label='kink z*=5000')
ax.axvspan(zstar-500, zstar+500, color='#b87333', alpha=0.15, label='excluded window')
ax.set_xlabel('月应纳税所得额'); ax.set_ylabel('每 bin 人数')
ax.set_title(f'Bunching: excess mass B={B:.0f}, e={e_hat:.3f}')
ax.legend(); plt.tight_layout(); plt.show()
06 论文案例
英文经典文献
中文顶刊与中文工作论文
07 常见错误
bin 太粗会把峰"磨平"、$B$ 被低估;bin 太碎会引入噪声、$B$ 被高估。必须报告多档 bin width(如 50/100/200)下的 $B$ 与 $\hat e$,并附 placebo kink 检验。
回归多项式时若不 excluded 拐点附近窗口,4 阶多项式会把"鼓包"当成真实分布趋势,导致 $\hat h_0(z^*)$ 偏高、$B$ 被严重低估。必须先选定 excluded window(通常靠图形判断或迭代法),再回归。
Saez (2010) 假设无摩擦。Chetty et al. (2011) 证明现实中个体因调整成本只能"近似"聚束,峰是弥散的。若直接用 Eq. 3.6 会低估真实弹性。中文文献(《世界经济》2024)已开始引入调整成本修正。
bin 是构造出来的,OLS 残差独立同分布假设不成立;多项式阶数、excluded window 选择也带不确定性。必须用个体层面 bootstrap(或 parametric bootstrap),报告 BCa / percentile 置信区间。
Notch 会产生"支配区间"空洞,必须先识别 dominated region 再补回;kink 没有空洞,直接拟合即可。混淆二者会错误地把"无人区间"当作真实分布的一部分。
在离真实拐点较远的位置随机设一个假 kink,重复同样流程,$B$ 应该不显著。如果 placebo kink 也显著,说明你看到的"峰"只是数据本身的波动或其他制度因素,bunching 识别无效。
08 与其他方法的交叉链接
- 与 RDD(07):同属"利用阈值做准实验"。RDD 看结果变量在 cutoff 的跳跃,bunching 看 running variable 密度在 cutoff 的隆起。两者都需要"无 manipulation"假设,都要做 rddensity / placebo 类检验。
- 与 IV(05)、DID(06):bunching 估计的是"边际个体"的局部弹性,外推到全体需要结构模型;DID/IV 估计的是处理效应。写论文时常把 bunching 作为机制检验——证明 bunching 峰确实由税率变化驱动。
- 与 非参/半参方法(29):反事实密度的多项式拟合本质是局部多项式非参数估计;多项式阶数选择、带宽选择与核回归的偏差—方差权衡同源。
- 与 合成控制(24):bunching 内部的"反事实分布"思想与 SCM 的"反事实合成"在哲学上一致——都是在没有 treatment 的世界里构造一条平滑基准线。