21.3 数据管线(拉取与清洗)
面向经管学生、研究者与从业者的 AI 智能体设计教材

规则基座立起来之后,项目正式进入八阶段流水线的阶段 2 与阶段 3——数据拉取与数据清洗。这两个阶段是 AI 智能体最能省力气的环节,也是最容易出隐性事故的环节:脚本写得快,但样本边界、聚合方式、异常值处理一旦悄悄变化,下游所有回归都会跟着漂。本节集中讲两件事:脚本化的数据拉取——公开数据怎么批量下载、订阅数据怎么分层隔离,让脚本进 Git、原始数据留本地;以及清洗子代理的职责边界——哪些机械操作可以默认放给 AI,哪些样本规则的拍板必须由研究者本人完成。
数据阶段的棘手主要来自数据来源的异质性。公开数据(如 BLS 的 QCEW、圣路易斯联储的 FRED)可以直接写进脚本、分发给第三方;订阅数据(WRDS/Compustat、Wind、CSMAR)受许可协议约束,原始数据不能上云;本地采集的调研数据或清洗中间产物,则介于两者之间。这三类数据在项目骨架里的路径、权限、是否进入 Git 仓库各不相同。把这件事想清楚,是让 AI 协作不出事故的前提。
学术数据库拉取的脚本化
公开数据的拉取应当全部写成脚本,让第三方可以复现。以 QCEW 为例,BLS 发布按年压缩包,每个包里是全行业、全地区的 CSV 文件,可以通过直连下载地址批量获取。一段 01_download.do 就能覆盖整个拉取流程,不需要手工点击。
* 01_download.do : QCEW 按年批量下载与格式转换
* 依赖:curl(Windows 10 自带)、Stata 18
version 18.0
clear all
set more off
local raw_dir "data/raw/qcew"
cap mkdir "`raw_dir'"
* 批量下载 2000-2019 年的县级 CSV 压缩包
forvalues y = 2000/2019 {
local url "https://data.bls.gov/cew/data/files/`y'/csv/`y'_annual_singlefile.zip"
local zip "`raw_dir'/qcew_`y'.zip"
if !fileexists("`zip'") {
shell curl -s -o "`zip'" "`url'"
shell unzip -o "`zip'" -d "`raw_dir'/"
}
}
* 循环导入并贴标签
tempfile master
save `master', emptyok replace
forvalues y = 2000/2019 {
local csv "`raw_dir'/`y'.annual.singlefile.csv"
import delimited "`csv'", varnames(1) clear stringcols(1 3 4)
keep if own_code == "5" /* 私人部门 */
keep area_fips industry_code year qtr annual_avg_emplvl ///
avg_annual_pay total_annual_wages
destring annual_avg_emplvl avg_annual_pay, replace force
append using `master'
save `master', replace
}
use `master', clear
label data "QCEW 私人部门年度面板 2000-2019, 下载自 BLS"
compress
save "data/raw/qcew_2000_2019.dta", replace
* 样本量守卫:如果观测数远低于预期,立即停下
assert _N > 500000这段脚本做三件事:按年循环下载、解压并用 import delimited 导入、合并后落盘到 data/raw/。fileexists 做幂等检查,重跑时不重复下载;assert _N > 500000 是样本量守卫,如果下载失败导致观测数异常,脚本会立刻停下。AI 改代码时最怕悄悄改变样本规模,assert 是最简单的防线。
FRED 的拉取更直接。Stata 社区包 freduse 已经封装好 FRED API 调用,一行命令就能把 CPI、失业率等时序拖回本地:
freduse CPIAUCSL UNRATE FEDFUNDS, clear
* freduse 返回的 daten 为 Stata daily 日期格式;月度数据按每月 1 日对齐
keep if daten >= td(01jan2000) & daten <= td(31dec2019)
save "data/raw/fred_macro.dta", replaceWRDS、Compustat、Wind、CSMAR 这类订阅数据性质不同。许可协议通常禁止把原始数据上传到第三方服务器,意味着不能把 data/raw/ 推到 GitHub,也不能让 Claude Code 把原始数据通过对话窗口传给模型。做法是把脚本和原始数据分层:脚本进 Git,数据留本地。.gitignore 显式排除敏感目录:
# .gitignore — 数据分层隔离
data/raw/ # 原始数据,不提交
data/external/*.dta # 订阅数据清洗中间品,不提交
!data/external/README.md # 仅保留说明文件
output/figures/
output/tables/
*.logdata/external/README.md 里写明数据来源、抽取日期、字段清单和访问方式,供后续复现者按图索骥。这样既满足许可协议,也让复现包具备自证能力。
WRDS(Compustat、CRSP、IBES)、Wind、CSMAR 的订阅协议明确禁止把原始数据复制到第三方服务器。Claude Code 在默认配置下,会话内容会传回 Anthropic 云端。把原始 dta 文件直接粘进对话窗口、或让 AI 读取 data/raw/ 里的订阅数据并摘录字段,都可能触发违规。安全做法是让 AI 只读取 data/processed/ 中已经去标识、已经聚合的中间产物。
清洗子代理的职责边界
数据清洗是 AI 自主度最容易被高估的环节。很多操作看起来机械,实际上每一步都嵌入了研究者的判断。把边界划清楚,AI 能省下大量时间;越界了,结论可能悄悄改变。
| 子代理自动可做 | 必须 HITL |
|---|---|
| 生成缺失值分布报告(逐变量 %、按年/行业) | 决定缺失是删除、插补还是保留 |
| 频率对齐(月度 → 季度、季度 → 年度) | 选择聚合方式(均值/总和/末期值) |
| 面板平衡检查(列出不平衡单元清单) | 决定是否剔除不平衡单元 |
| 重复行检测(按主键组合) | 决定保留哪条重复记录 |
| 异常值标记(3σ 或分位数法则) | 决定删除、winsorize 还是保留 |
| 生成清洗日志(每步样本量变化) | 决定每步操作的先后顺序 |
左栏是 AI 可以默认执行的操作,结果以报告或标记形式呈现,不直接改变数据;右栏是研究者必须亲自拍板的决策,因为这些选择会改变估计样本。举个例子:在最低工资研究里,是否剔除阿拉斯加州(远低于人均收入均值、产业结构特殊)会直接影响主回归系数。AI 可以告诉你 “阿拉斯加在分布右尾、3σ 之外”,但不能替你决定它该不该留。
更稳妥的做法是在 CLAUDE.md 或 .claude/rules/data-processing.md 里写死左栏的默认行为:异常值只标记不删除、缺失只报告不插补、面板不平衡只列表不剔除。AI 在清洗脚本里遇到右栏决策时,用 AskUserQuestion 工具让研究者明确指定,而不是自行填入默认值。
opencode 实录:从一句话到清洗脚本
下面是一段典型的交互。研究者已经跑完 01_download.do,准备进入清洗阶段。
/data-clean data/raw/qcew_2000_2019.dta 要和 data/external/state_min_wage.dta
合并成州-年面板。州码用 state_id,年份 year。结果变量是 log_emp
(每州每年总就业的对数)。帮我写 02_clean.do。
Claude Code 先读取两个文件的键和字段结构,确认 QCEW 键为 area_fips + industry_code + year + qtr(N = 6.2M),最低工资历史键为 state_id + year(N = 1020),需先按 state_id 聚合到州-年粒度再 1:1 merge。随后它列出三个决策点请研究者确认:聚合方式(sum 还是 mean)、行业范围(是否限定低薪行业)、不平衡单元(阿拉斯加和怀俄明 2001-2003 缺报是否剔除)。研究者分别回复”sum”“保留全行业”“列出缺报清单,暂时保留”后,Claude Code 生成 02_clean.do,末尾加三条 assert(样本量、无负值、主键唯一),同时输出清洗日志。
整个过程里研究者没写一行 Stata,但所有判断性决定都在他手上。
Stata 清洗脚本范例
按上面确认的规范,生成的 02_clean.do 核心片段如下:
* 02_clean.do : QCEW 与各州最低工资历史的合并与清洗
* 输入:data/raw/qcew_2000_2019.dta, data/external/state_min_wage.dta
* 输出:data/processed/panel_balanced.dta
version 18.0
clear all
set more off
set seed 20260417
log using "output/logs/clean_log.txt", replace text
use "data/raw/qcew_2000_2019.dta", clear
* 1. 提取州码(area_fips 前两位)
gen str2 state_id = substr(area_fips, 1, 2)
destring state_id, replace
* 2. 聚合到州-年粒度,就业取总和
collapse (sum) emp = annual_avg_emplvl (mean) pay = avg_annual_pay, ///
by(state_id year)
gen log_emp = ln(emp)
* 3. 缺失报告
misstable summarize, all
count if missing(emp)
local n_miss = r(N)
display "缺失就业记录:`n_miss'"
* 4. 面板平衡检查(不剔除,只输出清单)
bys state_id: gen n_years = _N
list state_id if n_years < 20, sepby(state_id) noobs
drop n_years
* 5. 合并各州最低工资历史(1:1)
tempfile qcew_clean
save `qcew_clean', replace
use "data/external/state_min_wage.dta", clear
merge 1:1 state_id year using `qcew_clean'
tab _merge
keep if _merge == 3
drop _merge
* 6. 异常值标记(不删除)
sum log_emp, detail
gen outlier_flag = abs(log_emp - r(mean)) > 3 * r(sd)
tab outlier_flag
* 7. 样本量守卫:AI 改代码时若偏离预期会立刻暴露
* 下界按 48 州 × 20 年估算(已排除阿拉斯加 2000-2002 等缺报单元)
count
assert _N >= 48 * 20
assert !missing(log_emp)
isid state_id year
compress
save "data/processed/panel_balanced.dta", replace
log close脚本里有三个习惯值得固化到所有清洗脚本里。第一,开头写 version 18.0 和 set seed,锁死 Stata 版本和随机种子,保证复现。第二,每步缺失检查、面板检查只报告不删除,保留研究者的决定权。第三,末尾用 assert 和 isid 做样本量与主键守卫。后者尤其关键:AI 反复迭代时,如果某次改动让观测数变了、或者主键不唯一,脚本在 save 之前就会中断,错误不会沉到下游。
清洗脚本末尾至少写三条 assert:总观测数等于预期(如州数×年数)、关键变量无缺失、主键唯一(isid)。AI 改代码时一旦偏离预期,脚本立即停下,不会把错误 dta 写入 data/processed/。这比事后发现系数奇怪再回溯省得多。
数据阶段的三大陷阱
数据阶段最常见的事故集中在三类。写在一起,方便清洗子代理的 Hooks 或 Review 检查清单里引用。
Off-by-one 边界错位。年份筛选 if year >= 2010 & year <= 2019 写成 > 2009 & < 2020 看似等价,但在变量类型从 int 漂成 float、或年份前补过空值时结果会不同。固定写法是始终用 >= 和 <=,并在 CLAUDE.md 里明示这一约定。AI 生成脚本时会严格跟随。
样本筛选错位。描述统计的样本和主回归的样本不一致是最隐蔽的错误。常见场景:在 03_descriptive.do 里对所有州做表,在 04_main_reg.do 里又 keep if state_id != 2 剔除阿拉斯加。两处样本数不同,审稿人一追问就要重跑。正确做法是所有 keep if 集中在 02_clean.do,下游脚本不再筛选;若必须在下游筛选,用显式的 preserve/restore 块,并在日志里记录。
聚类层级错配。csdid 默认使用影响函数(influence function)方法估计方差,若需要聚类标准误须显式指定 wboot 或 vce(cluster state_id);reghdfe 默认不聚类;sdid 另有 vce(bootstrap)。同一研究的标准误不一致,审稿人会要求全部重跑。做法是在 .claude/rules/regression-spec.md 里写死项目默认聚类层级(例如”一律 vce(cluster state_id) 或 wboot reps(999)“),所有 Skill 的估计步骤按此设置。主回归与稳健性共用同一个聚类规范,比逐个脚本设更稳。
三者的共性是边界模糊,错了以后很难发现。Hooks 可以在 Write 和 Edit 的时刻拦截这些特征——例如扫描新写入的 do 文件里是否有裸的 if year > 或 vce(robust),有则提示。Hooks 不能替代人审,但能把这些坑堵在 AI 写脚本的瞬间,比跑完回归再回溯便宜太多。