名师讲堂|使用 Stata 测算各区县鸟类丰度指数

今天给大家分享使用 Stata 测算各区县鸟类相对丰度指数的方法。配套的数据来自中国观鸟记录中心(RStata 数据中心爬取),核心方法是复现 Liang et al. (2020, PNAS) 提出的「努力量校正后的相对鸟类丰度」指数。本次以 2015–2020 年 的观鸟数据为例进行演示——样本窗口是出于示例目的选定的,并非参照某篇论文来确定。

完整的观鸟数据可以从下面的链接获取:

1980~2025 年观鸟记录、经纬度及其所处的省市区县数据(2026年3月爬取):https://rstata.duanshu.com/#/brief/course/0005692119984a0b90677782a8f0a333

下面先讲清楚这个指标到底是怎么算出来的,再逐段讲解计算代码。

一、指标来源与计算过程

1.1 论文背景:从空气污染管制到鸟类保护

Liang et al. (2020) 发表在 PNAS 的论文 *”Conservation cobenefits from air pollution regulation: Evidence from birds”* 想回答一个问题:美国《清洁空气法》削减空气污染,是否顺带保护了鸟类?

论文用的是 eBird 公民科学观鸟数据,覆盖全美各县。它的核心因变量不是「鸟类丰富度(species richness)」,而是 「努力量校正后的相对鸟类丰度」。这个指数把「观察者有多努力」「当时好不好看见」这些干扰剥离掉,只留下真正反映鸟类数量的信号,再用来和臭氧、PM2.5 做回归。

一句话:论文要的是「相对」丰度,不是「绝对」有多少只鸟——它只能告诉你 A 地比 B 地、今年比去年多还是少。

1.2 两步法(Methods: Bird Abundance Estimation)

1.3 本讲义如何用中国观鸟数据复现

数据来自 RStata 数据中心「1980~2025 年观鸟记录、经纬度及其所处的省市区县数据」,这里选择 2015~2020 年的数据样本演示。包含两份核心 dta:

  • 观鸟记录表(report 级):约等于论文的 “checklist”;
  • 鸟种观测统计报告(report×物种级):约等于 eBird Reference Dataset;

关键:两份数据里 鸟种数量 含义不同!report 级是「物种数」,report×物种级是「个体数」。论文 Eq.1 左边的 #birds observed = 明细表按 reportId 求和的个体数。

论文分类群划分(Rosenberg et al. 2019 口径,映射到中文目名)如下:

  • 水禽 waterfowl:雁形目;
  • 涉禽 shorebirds:鸻形目;
  • 水鸟 waterbirds:鹈形目、鹤形目、䴙䴘目、鲣鸟目、鹳形目、潜鸟目、鹱形目、鹲形目、红鹳目;
  • 其余目 = 陆鸟 landbirds。

本讲义以 2015–2020 年的数据为例进行演示。更早年份(2015 之前)数据极稀疏,县×月×年固定效应难以识别,故不纳入本示例窗口。

二、详细讲解计算代码

整套计算分三步,对应三个 Stata 脚本:01_筛选样本 → 02_测算丰度指数 → 03_对照刘钊论文。下面逐段给出完整代码并讲解。

2.1 步骤 1:筛选 2015–2020 样本

观鸟记录表按 观测时间_起始 的年份直接筛选;鸟种观测统计报告本身无时间字段,需按 2015–2020 的 reportId 集合回连筛选。

1980~2025年观鸟记录、经纬度及其所处的省市区县数据(2026年3月爬取) 文件夹在附件中未提供,如需要可以从前述链接获取。

*- ==============================================================================
*- 步骤 1:从原始数据中筛选 2015–2020 年样本,导出到本项目文件夹
*- 数据处理:微信公众号 RStata
*- 数据来源:RStata 数据中心「1980–2025 年观鸟记录、经纬度及其所处的省市区县数据」
*- - 观鸟记录表(report 级):直接按 观测时间_起始 的年份筛选
*- - 鸟种观测统计报告(report×物种级):本身无时间字段,需按 2015–2020 的
*- reportId 集合回连筛选
*- 产出:本项目文件夹下的两个样本 dta
*- - 观鸟记录_2015_2020.dta
*- - 鸟种观测统计报告_2015_2020.dta
*-
*- 说明:本脚本读取「原始 1980–2025 数据」重新生成样本。若本项目文件夹中已存在
*- 上述两个样本 dta(随项目一并提供),可直接跳过本脚本,运行 02 / 03 即可。
*- ==============================================================================

clear all
set more off
set seed 20260601

*- ---- 路径 ----
*- 原始数据所在文件夹(本机已爬取的 1980–2025 全量数据)
global dir_in_raw "/Users/ac/Desktop/计算地区生物多样性/1980~2025年观鸟记录、经纬度及其所处的省市区县数据(2026年3月爬取)"
*- 本项目(Stata)文件夹
global dir_out "/Users/ac/Desktop/使用 Stata 测算各区县鸟类丰度指数"
global dir_tmp "$dir_out/_intermediate"
capture mkdir "$dir_tmp"

*- 输入/输出文件直接使用实体文件名(见下方 use / save 语句)

local YEAR_MIN 2015
local YEAR_MAX 2020

*- ------------------------------------------------------------------------------
*- 1) 观鸟记录表:按年份筛选
*- ------------------------------------------------------------------------------
display "== 步骤1:读取观鸟记录表并按年份筛选 =="

use "$dir_in_raw/1980~2025年观鸟记录、经纬度及其所处的省市区县数据(2026年3月爬取).dta", clear
*- 观测时间_起始 为 %tc(毫秒),用 dofc() 转成日期后取年份
gen double _date0 = dofc(观测时间_起始)
gen int _year = year(_date0)
keep if inrange(_year, `YEAR_MIN', `YEAR_MAX')
display "筛选后报告数: " _N
*- 年份分布(仅展示,不改变数据集)
preserve
gen byte _one = 1
collapse (sum) n = _one, by(_year)
list _year n, sep(0) noobs
restore
drop _date0

save "$dir_out/观鸟记录_2015_2020.dta", replace
display "已写出 -> $dir_out/观鸟记录_2015_2020.dta"

*- ------------------------------------------------------------------------------
*- 2) 鸟种观测统计报告:按 reportId 关联筛选(只保留 2015–2020 的报告)
*- ------------------------------------------------------------------------------
display _n "== 步骤2:按 reportId 关联筛选鸟种观测统计报告 =="

*- 取已筛选报告的 reportId 集合
preserve
use "$dir_out/观鸟记录_2015_2020.dta", clear
keep reportId
*- 中间数据集:$dir_tmp/rep_ids_2015_2020.dta
save "$dir_tmp/rep_ids_2015_2020.dta", replace
restore

use "$dir_in_raw/1980~2025年鸟种观测统计报告(2026年3月爬取).dta", clear
merge m:1 reportId using "$dir_tmp/rep_ids_2015_2020.dta", keep(match) nogenerate
save "$dir_out/鸟种观测统计报告_2015_2020.dta", replace
display "已写出 -> $dir_out/鸟种观测统计报告_2015_2020.dta(观测记录 " _N " 行)"

display _n "步骤1 完成:两个样本 dta 已导出至 $dir_out"

2.2 步骤 2:测算各区县鸟类相对丰度指数(复现 Liang et al. 2020)

读取样本后,先把鸟种明细(report×物种级)按 reportId 汇总成「报告级」的总个体数 n_ind_all 与物种数 n_sp_all,并切出四个分类群;再把报告表整理出努力量(hours、n_observers)与地理时间维度,最后按 reportId 合并、做样本筛选、跑固定效应泊松回归取出 cmy 固定效应、z 标准化、合成县×月×年面板并写出带中文标签的 dta,同时绘制三张诊断图。

*- 此处代码需要下载讲义材料查看~

核心的 ppmlhdfe 取 cmy 固定效应写法里,absorb(cmy_n hour_day) 把 hour_day(可探测性)和 cmy(县×月×年)吸收掉,不报系数,但会算出估计值;d(_d) 选项保存该观测对应两类固定效应之和 _d = α_cmy + ζ_h,再取「单元内均值(_d) − 总均值(_d)」即得 Γ^cmy 的估计(与真正的 α_cmy 仅差一个常数,z 标准化后完全等价,已与 R/Python 版验证 corr≈0.999)。

图 1 展示全国县级相对鸟类丰度在努力量校正前后的对比:

图 2 从「努力量(时长)」和「可探测性(观测时刻)」两个角度说明为什么必须做努力量校正:

图 3 展示论文口径的四个分类群(水禽 / 涉禽 / 水鸟 / 陆鸟)相对丰度指数:

2.3 步骤 3:与刘钊等(2025)论文趋势对照

刘钊等 (2025, 经济学(季刊)) 以「绿色金融改革创新试验区(2017)」为准自然实验,核心结论是试验区鸟类丰富度显著提升;其被解释变量明确「借鉴 Liang et al. (2020) 计算得到并标准化」。下面用我们产出的面板做方向性(naive DiD)检验,看趋势是否一致。

*- ==============================================================================
*- 步骤 3:对照刘钊等 (2025, 经济学季刊) 的趋势一致性检验
*- 数据处理:微信公众号 RStata
*- 论文:绿色金融改革创新试验区(2017)显著提升鸟类丰富度(借鉴 Liang et al.2020 方法)
*- 本脚本用「02_测算鸟类丰度指数_2015_2020.do」产出的县×月×年面板(样本 2015–2020),
*- 做方向性(naive DiD)与生态学合理性检验。
*- ==============================================================================

clear all
set more off
set seed 20260601

global dir_in "/Users/ac/Desktop/使用 Stata 测算各区县鸟类丰度指数"
global dir_out "$dir_in/output"

*- 输入面板直接使用实体文件名(见下方 use 语句)

use "$dir_out/panel_county_month_year_2015_2020.dta", clear
summarize year
local ymin = r(min)
local ymax = r(max)
display "面板: " _N " 县×月×年单元; 年份 " `ymin' " - " `ymax'

*- ---- 1. 试验区定义(首批准试验区,2017年6月)----
*- 赣江新区→南昌/九江;贵安新区→贵阳;花都→广州。整市视为处理组(偏保守,稀释效应)。
gen byte treat = inlist(city, "湖州市", "衢州市", "南昌市", "九江市", ///
"广州市", "贵阳市", "兰州市", "哈密市")
gen byte post = (year >= 2017)

count if treat == 1
display "处理组城市(县×月×年单元数): " r(N) " 对照组: " _N - r(N)

*- ---- 2. 朴素双重差分 (2015-2020, 政策前=2015-2016, 后=2017-2020) ----
gen byte period = (year <= 2016) // 1=政策前(pre, ≤2016),0=政策后(post, ≥2017);下方 diff = g0 - g1 = 政策后 - 政策前

preserve
collapse (mean) g = gstd_all [aw=n_checklist], by(treat period)
reshape wide g, i(treat) j(period)
gen diff = g0 - g1 // 标准 DiD = (政策后 - 政策前)|处理组 - (政策后 - 政策前)|对照组
summ diff if treat == 1, meanonly
local dt = r(mean)
summ diff if treat == 0, meanonly
local dn = r(mean)
local naive_did = `dt' - `dn'
display _n "朴素 DiD (丰度指数) = " %9.4f `naive_did'
display "论文表1 DID系数 β1 显著为正 → 若本指数方向一致,应 >0。"
*- 输出到标量文件,供讲义/日志引用
local naive_did_s : display %9.4f `naive_did'
restore

*- 物种丰富度口径的朴素 DiD (对应论文表3中的“鸟种丰富度”)
preserve
collapse (mean) s = sp_richness [aw=n_checklist], by(treat period)
reshape wide s, i(treat) j(period)
gen diff = s0 - s1 // 同 DiD 口径:政策后 - 政策前
summ diff if treat == 1, meanonly
local dt = r(mean)
summ diff if treat == 0, meanonly
local dn = r(mean)
local naive_did_sp = `dt' - `dn'
display _n "朴素 DiD (物种数 richness) = " %9.4f `naive_did_sp'
display "注意:本物种数为未做努力量校正原始计数,论文种类异质性一般也做 Liang 校正。"
local naive_did_sp_s : display %9.4f `naive_did_sp'
restore

*- ---- 3. 事件研究式年度轨迹 (2015-2020) ----
preserve
collapse (mean) g = gstd_all [aw=n_checklist], by(treat year)
twoway ///
(connected g year if treat == 0, lcolor(gs12) lpattern(solid) mcolor(gs12) lwidth(*1.2)) ///
(connected g year if treat == 1, lcolor("228 26 28") lpattern(dash) mcolor("228 26 28") lwidth(*1.8)), ///
legend(order(1 "非试验区" 2 "试验区") size(*0.85)) ///
xline(2016.5, lpattern(dot) lcolor("228 26 28")) ///
xtitle("年份", size(small)) ytitle("相对丰度指数(标准化)均值", size(small)) ///
title("图A 试验区 vs 非试验区 年度丰度轨迹", size(medium) color(black)) ///
subtitle("数据处理 & 绘制:微信公众号 RStata", size(small)) ///
note("红色虚线 = 2017 年政策实施;数据来源:RStata 数据中心(样本 2015–2020)", size(vsmall)) ///
ylabel(, format(%9.2f)) xlabel(2015(1)2020, format(%9.0f)) ///
graphr(margin(medium))
graph save `"$dir_out/_figA"', replace
restore

*- ---- 4. 生态学合理性:水鸟(候鸟为主)冬季峰值、留鸟(陆鸟)较平稳 ----
preserve
collapse (mean) water = gstd_waterbird (mean) land = gstd_landbird [aw=n_checklist], by(month)
twoway ///
(connected water month, lcolor("55 126 184") mcolor("55 126 184") lwidth(*1.5)) ///
(connected land month, lcolor("228 26 28") mcolor("228 26 28") lwidth(*1.5)), ///
legend(order(1 "水鸟(多为候鸟)" 2 "陆鸟(多为留鸟)") size(*0.85)) ///
xtitle("月份", size(small)) ytitle("相对丰度指数(标准化)均值", size(small)) ///
title("图B 季节模式:水鸟冬季(11-2月)峰值", size(medium) color(black)) ///
subtitle("数据处理 & 绘制:微信公众号 RStata", size(small)) ///
note("与「候鸟冬季南下」生态事实一致;数据来源:RStata 数据中心(样本 2015–2020)", size(vsmall)) ///
ylabel(, format(%9.2f)) xlabel(1(1)12, format(%9.0f)) ///
graphr(margin(medium))
graph save `"$dir_out/_figB"', replace
restore

*- ---- 5. 分类群年度趋势 (2015-2020) ----
preserve
collapse (mean) gstd_waterfowl gstd_shorebird gstd_waterbird gstd_landbird [aw=n_checklist], by(year)
twoway ///
(connected gstd_landbird year, lcolor("228 26 28") mcolor("228 26 28") lwidth(*1.5)) ///
(connected gstd_waterfowl year, lcolor("55 126 184") mcolor("55 126 184") lwidth(*1.5)) ///
(connected gstd_shorebird year, lcolor("77 175 74") mcolor("77 175 74") lwidth(*1.5)) ///
(connected gstd_waterbird year, lcolor("152 78 163") mcolor("152 78 163") lwidth(*1.5)), ///
legend(order(1 "陆鸟 landbirds" 2 "雁鸭类 waterfowl" ///
3 "鸻鹬鸥类 shorebirds" 4 "其他水鸟 waterbirds") size(*0.85)) ///
xtitle("年份", size(small)) ytitle("相对丰度指数均值", size(small)) ///
title("图C 四大分类群年度趋势", size(medium) color(black)) xlabel(2015(1)2020, format(%9.0f)) ///
subtitle("数据处理 & 绘制:微信公众号 RStata", size(small)) ///
note("数据来源:RStata 数据中心(样本 2015–2020)", size(vsmall)) ///
ylabel(, format(%9.2f)) ///
graphr(margin(medium))
graph save `"$dir_out/_figC"', replace
restore

*- ---- 6. 区域基线(描述性上下文) ----
*- 省份 -> 区域(东部/中部/西部/东北/其他)
gen region = "其他"
replace region = "东部" if prov == "北京市" | prov == "天津市" | prov == "河北省" | prov == "上海市" | ///
prov == "江苏省" | prov == "浙江省" | prov == "福建省" | prov == "山东省" | ///
prov == "广东省" | prov == "海南省"
replace region = "中部" if prov == "山西省" | prov == "安徽省" | prov == "江西省" | prov == "河南省" | ///
prov == "湖北省" | prov == "湖南省" | prov == "广西壮族自治区"
replace region = "西部" if prov == "内蒙古自治区" | prov == "重庆市" | prov == "四川省" | prov == "贵州省" | ///
prov == "云南省" | prov == "西藏自治区" | prov == "陕西省" | prov == "甘肃省" | ///
prov == "青海省" | prov == "宁夏回族自治区" | prov == "新疆维吾尔自治区"
replace region = "东北" if prov == "辽宁省" | prov == "吉林省" | prov == "黑龙江省"

preserve
collapse (mean) g = gstd_all [aw=n_checklist], by(region)
keep if inlist(region, "东部", "中部", "西部", "东北")
graph bar g, over(region, sort(g) label(angle(0) labsize(*0.8))) ///
ytitle("相对丰度指数均值", size(small)) ///
title("图D 区域基线丰度(描述性)", size(medium) color(black)) ///
subtitle("数据处理 & 绘制:微信公众号 RStata", size(small)) ///
note("数据来源:RStata 数据中心(样本 2015–2020)", size(vsmall)) ///
bar(1, color("55 126 184")) ///
ylabel(, format(%9.2f)) graphr(margin(medium))
graph save `"$dir_out/_figD"', replace
restore

*- ---- 输出 ----
graph combine `"$dir_out/_figA.gph"' `"$dir_out/_figB.gph"' `"$dir_out/_figC.gph"' `"$dir_out/_figD.gph"', ///
rows(2) cols(2) ///
title("与刘钊等 (2025) 论文趋势对照", size(medium) color(black)) ///
subtitle("数据处理 & 绘制:微信公众号 RStata", size(small)) ///
note("年度轨迹 / 季节模式 / 分类群趋势 / 区域基线;数据来源:RStata 数据中心「1980~2025 年观鸟记录、经纬度及其所处的省市区县数据」(样本 2015–2020)", size(vsmall)) ///
graphr(margin(medium)) xsize(20) ysize(12)
graph export `"$dir_out/fig4_与刘钊论文对照_2015_2020.png"', width(4800) replace
erase `"$dir_out/_figA.gph"'
erase `"$dir_out/_figB.gph"'
erase `"$dir_out/_figC.gph"'
erase `"$dir_out/_figD.gph"'
display _n "已保存 $dir_out/fig4_与刘钊论文对照_2015_2020.png"

*- ---- 描述性趋势汇总 ----
display _n "=== 描述性趋势汇总 ==="
preserve
collapse (mean) raw = gamma_hat [aw=n_checklist], by(year)
list year raw, sep(0) noobs
restore
display _n "分类群 2015 vs 2020 变化(标准化指数):"
preserve
keep if inlist(year, 2015, 2020)
collapse (mean) gstd_all gstd_waterbird gstd_landbird [aw=n_checklist], by(year)
list year gstd_all gstd_waterbird gstd_landbird, sep(0) noobs
restore

display _n "全部完成。"

下图把我们的指数与刘钊等 (2025) 论文做趋势一致性对照(年度轨迹、季节模式、分类群趋势、区域基线):

解读:丰度指数口径的朴素 DiD > 0,与刘钊等 (2025) 表 1 中 DID 系数 β1 显著为正的方向一致;而未校正物种数口径的 DiD 为负,是因为非试验区观测基数增长更快——这正说明必须用努力量校正后的指数做比较,不能拿原始物种数直接比。

运行说明:本讲义的 .do 代码为演示展示用;实际需先安装 ppmlhdfe(ssc install ppmlhdfe),再依次运行 01→02→03(或 do "00_运行全部.do")以生成 output/ 下的面板与四张结果图。

点击这里跳转到 RStata 短书平台获取附件:名师讲堂|使用 Stata 测算各区县鸟类丰度指数

评论