数据合并与面板整理:从零散表到公司-年份面板
merge 1:1 / 1:m / m:1 三种连接、append 纵向追加、reshape 在 long/wide 之间切换、xtset 面板设定、xtdescribe 检查平衡度、行业代码跨年度匹配——把多张表整合成一张可回归的分析样本。
merge、append、reshape、xtset、xtdescribe;本页不依赖额外外部命令)。01 为什么"合并"是面板数据的核心工程问题
在中文实证研究里,你几乎永远不会从一个 .dta 文件开始。你会有一堆来源不同、粒度不同、时间覆盖不同的表:CSMAR 的财务报表表、Wind 的股价表、国家统计局的行业层面表、手工收集的政策冲击表。把它们整合成"公司 i × 年份 t"的矩形面板,是描述性统计和回归之前必须完成的工程。
合并错了,后面所有结论都会错。最常见的两类错误:(1) merge 模式选错,把一对一的表按多对一合并,结果同一行被复制成多行,样本量莫名其妙翻倍;(2) 关键字段没对齐,股票代码在不同年份编码规则变了,导致匹配率(match rate)看起来正常但其实是张冠李戴。本节把这两件事拆透。
02 merge 三种连接:1:1 / 1:m / m:1
Stata 的 merge 命令按"两边关键字段的唯一性"分三种模式。理解这三种模式的关键,是先问自己一个问题:主数据集(master)和被合并数据集(using)里,每个关键键值是唯一的还是会重复?
- 1:1:两边每个键值都唯一。典型场景:公司基本信息表(每家公司一行)与公司治理结构表(每家公司一行)合并。
- m:1:master 里一个键值多行,using 里每个键值一行。典型场景:你已经有公司-年份面板,要给每一行"贴上"行业代码(行业表是公司层面的,每家公司一行)。
- 1:m:master 里每个键值一行,using 里多行。很少用,方向与 m:1 相反——通常把 using 放前面反过来写。
很多新手上手会写 merge m:m id using ...。Stata 明确拒绝这种写法——它的官方文档把 m:m 称为"deprecated and dangerous"。正确做法是先想清楚两边的真正粒度,然后降级成 1:1 / 1:m / m:1。
*--------------------------------------------------------------*
* 02-merge-modes.do
*--------------------------------------------------------------*
clear all
set more off
* ---------- 构造演示数据 ----------
* master: 公司-年份面板(已清洗好的财务数据)
clear
input stkcd year roe
1 2010 0.05
1 2011 0.08
2 2010 0.03
2 2011 0.04
3 2010 0.09
end
save "master_fin.dta", replace
* using-A: 公司基本信息表(每家公司一行)—— 1:1 / m:1 都可以用
clear
input stkcd industry province
1 "C3" "Guangdong"
2 "C4" "Beijing"
end
save "using_info.dta", replace
* using-B: 行业层面宏观变量(每个行业-年份一行)
clear
input industry year ind_growth
"C3" 2010 0.12
"C3" 2011 0.15
"C4" 2010 0.08
"C4" 2011 0.09
end
save "using_indmacro.dta", replace
* ---------- 1:1 合并:公司基本信息 ----------
* 因为 master 里 stkcd-year 唯一、using 里 stkcd 唯一,
* 这里用 m:1 stkcd(master 多行、using 一行)最自然
use "master_fin.dta", clear
merge m:1 stkcd using "using_info.dta"
* _merge 取值:
* 1 = 主数据集独有(using 没匹配到)
* 2 = 被合并数据集独有(master 没这行)
* 3 = 两边都有(完美匹配)
* 查看匹配情况
tab _merge
* 删掉"using 独有"的行(对我们没意义)
keep if _merge == 1 | _merge == 3
drop _merge
* ---------- 多关键字 m:1:行业 × 年份 ----------
* 这次要合并的是"行业-年份"层面的宏观变量,
* 主数据集中没有 industry,需要先在 master 里加 industry 列。
* 为演示,我们直接重建 master:
use "master_fin.dta", clear
gen industry = ""
replace industry = "C3" if stkcd == 1
replace industry = "C4" if stkcd == 2
replace industry = "C4" if stkcd == 3
* 按 (industry, year) 两个关键字合并
merge m:1 industry year using "using_indmacro.dta"
tab _merge
drop _merge
list
03 理解 _merge:1 / 2 / 3 三种结果
每次 merge 之后,Stata 会自动生成一个叫 _merge 的变量,告诉你每一行的来源。读懂 _merge 的分布,是判断合并是否成功的关键。
| _merge 取值 | 含义 | 实务含义 |
|---|---|---|
_merge == 1 | 主数据集 (master) 有、using 没有 | 这一行没找到对方的数据,新变量会是缺失 |
_merge == 2 | using 有、master 没有 | 被合并表里多出的行,对主样本无意义,通常直接删 |
_merge == 3 | 两边都有(完美匹配) | 理想情况,新变量成功贴到对应行上 |
一个经验法则:合并完先 tab _merge,把 _merge==3 的占比作为"匹配率"。如果你的匹配率不到 80%,要回头检查关键字段是否对齐(股票代码是否带前导零、行业代码是否跨年份换了标准)。
04 多关键字匹配与行业代码对齐
真实研究里最常见的匹配痛点,是行业代码跨年份不一致。中国证监会 2001 年、2012 年两次修订《上市公司行业分类指引》,旧代码(如"C8"医药)和新代码(如"C27"医药)的映射并不一一对应。直接按 industry 合并,会把 2011 年的"C8"贴到 2012 年的"C8"上,而后者在新口径下可能已经变成了别的行业。
industry_map.dta:每行一对 (old_code, new_code, broad_category)。这一步是一次性体力活,但收益巨大。*--------------------------------------------------------------*
* 04-industry-match.do
*--------------------------------------------------------------*
* 假设 master 里有 stkcd year industry
* 2011 年及以前用老版(CSRC 2001),2012 起用新版(CSRC 2012)
* 1) 把年份分段
gen old_code_flag = (year <= 2011)
* 2) 准备两张映射表(省略手工构造代码,假设已经在 industry_map_2001.dta 和 industry_map_2012.dta 里)
* use industry_map_2001.dta, clear
* keep if old_code_flag == 1
* merge m:1 industry using ...
* 3) 一个更实用的写法:把行业代码截断成"证监会一位大类"
* 旧代码形如 "C8",新代码形如 "C27"
gen ind1 = substr(industry, 1, 1) // 把 C8 / C27 都变成 "C"
* 三位行业大类:旧版取前两位,新版取前一位加两位数字
gen ind3 = ""
replace ind3 = substr(industry, 1, 2) if year <= 2011
replace ind3 = substr(industry, 1, 3) if year >= 2012
* 4) 行业 × 年份固定效应:用 ind3 × year
egen ind_year = group(ind3 year)
* 之后 reghdfe y x, absorb(ind_year firm) 即可
05 append:纵向堆叠不同年份
append 与 merge 完全不同:merge 是横向拼接(增加列),append 是纵向堆叠(增加行)。当你按年份分别下载了 2007–2020 年的财务表,每个文件结构相同,就用 append 把它们摞起来。
*--------------------------------------------------------------*
* 05-append.do
*--------------------------------------------------------------*
clear all
set more off
* 假设 data/raw/ 下有 fin_2007.dta ... fin_2020.dta
* 标准做法:先清空,再 append using
use "data/raw/fin_2007.dta", clear
forvalues y = 2008/2020 {
append using "data/raw/fin_`y'.dta"
}
describe
count
* 关键检查:append 后变量名应当完全一致
* 如果有"2015 年新增了一个变量 R&D",旧年份该变量会自动变成缺失
* 用 misstable summarize 检查一下
06 reshape long/wide:宽表与长表互转
很多下载下来的数据是 wide 形式:每家公司一行,每年一个变量列(如 roe_2007 roe_2008 ... roe_2020)。但 Stata 的面板命令(xtset、xtreg、reghdfe)要求 long 形式:每行是一个 (公司, 年份) 观测。reshape 命令就是在这两种形态之间切换的瑞士军刀。
*--------------------------------------------------------------*
* 06-reshape.do
*--------------------------------------------------------------*
clear all
set more off
* ---------- 第一步:构造一个 wide 表 ----------
* 每家公司一行,roe_2010 ... roe_2012 三列
clear
input stkcd roe_2010 roe_2011 roe_2012
1 0.05 0.08 0.06
2 0.03 0.04 0.05
3 0.09 0.10 0.11
end
list
* ---------- wide -> long(最常用!) ----------
* reshape long 告诉 Stata:
* "把 roe_<j> 拆成 (j, roe)"
* 语法:reshape long 变量前缀, i(id) j(timevar)
reshape long roe, i(stkcd) j(year)
list, sepby(stkcd)
* 现在是 (stkcd, year, roe) 的长表
* ---------- long -> wide(反向操作) ----------
reshape wide roe, i(stkcd) j(year)
list
报错 1:"data have multiple observations within (stkcd) that are not unique" —— 说明你的 (i, j) 组合已经重复了,先 duplicates drop。
报错 2:"j variable (year) contains negative values" —— reshape 要求 j 是正整数年份。如果原始列名是 roe_2010,Stata 会自动把 2010 解析成 year;如果是 roe_a roe_b,需要先把字母 j 编码成数字。
07 xtset 面板设定与 xtdescribe
一旦你有了 (stkcd, year) 长表,下一步就是告诉 Stata"这是一个面板数据":xtset panelvar timevar。这一步做完,所有 xt 开头的命令(xtreg、xtsum、xtdescribe、xtsum 等)才能识别面板结构。
*--------------------------------------------------------------*
* 07-xtset.do
*--------------------------------------------------------------*
webuse nlswork, clear // Stata 自带的面板:员工-年份
* 1) xtset:声明 panelvar = idcode, timevar = year
xtset idcode year
* 2) xtdescribe:检查面板的"形状"
* 输出会告诉你:
* - 总共多少个组(公司/个人)
* - 时间跨度
* - 是否平衡(balanced)
* - 每个组有多少期观测
xtdescribe
* 3) xtsum:面板版的 summarize(组内/组间/总体方差分解)
xtsum ln_wage age tenure
* 4) 看一下"时间有没有跳跃"
* 比如某个员工 1980 年之后跳到 1985 年,xtset 会警告
list idcode year in 1/30
重点看三块:① "n = 4711" 是组(公司)个数;② "T = 12" 是时间维度;③ Distribution of T_i 表格——如果大多数组都有 12 期、少数只有几期,说明是非平衡面板(unbalanced panel)。现代实证方法(reghdfe、xtreg fe)都默认支持非平衡,你不需要强行把它变成平衡。
08 平衡 vs 非平衡面板的转换
平衡面板(balanced panel)指每个公司都恰好有 T 期观测;非平衡面板(unbalanced panel)指有的公司只观察到几年、有的公司观察了全部年份。中国上市公司面板几乎从来不是平衡的——IPO、退市、合并报表口径变化都会让某个公司"消失"几年。
什么时候需要转成平衡面板?通常只有两种情况:(1) 跑差分 GMM / 系统 GMM(xtabond2),这些方法对非平衡面板敏感;(2) 做一些特殊的稳健性检验。主回归一般用非平衡面板即可,无需强求平衡。
*--------------------------------------------------------------*
* 08-balanced.do
*--------------------------------------------------------------*
webuse nlswork, clear
xtset idcode year
xtdescribe // 此时是非平衡
* 方法:只保留"恰好观察满所有年份"的公司
* 1) 每个公司有多少期?
bysort idcode: gen n_years = _N
* 2) 总年数是多少?
quietly levelsof year, local(allyears)
scalar T_total = `: word count `allyears''
* 3) 保留恰好等于 T_total 的公司
keep if n_years == T_total
xtset idcode year
xtdescribe // 现在应当是 "balanced"
09 论文案例:合并策略如何影响识别
10 常见错误与调试
第二次再 merge 同名关键字时,Stata 会抱怨"_merge already defined"。规范做法:每次 merge 后 drop _merge,或者用 merge ... , keep(match master) noreport 一次性处理。
Stata 读 Excel 时会把 "000001" 读成数字 1,下次与字符串型股票代码 merge 永远匹配不上。预防:(1) 导入时用 import excel ..., cellranges(...) clear 后立刻 tostring stkcd, format(%06.0f) replace;(2) merge 前两边都用 gen stkcd_str = string(stkcd, "%06.0f") 标准化。
append 要求两边变量类型一致。如果某一年的 year 是字符串、另一年是数值,append 后整个 year 变成字符串,xtset 报错。预防:append 前 describe 一遍,或在每个文件里就强制 destring year, replace。
reshape 只是改变数据形状,不会自动告诉 Stata 面板结构。做完 reshape 必须 xtset stkcd year,否则后面 xtreg / reghdfe 报错 "not sorted" 或直接把数据当截面跑。
11 进阶学习资料
- Stata 官方:
help merge、help append、help reshape、help xtset、help xtdescribe。其中 merge 的 PDF 手册附录里有非常详细的"为什么 m:m 是错的"。 - 教材:Cameron & Trivedi, Microeconometrics Using Stata 第 2 章(data management);陈强《高级计量经济学及 Stata 应用》第 11 章"面板数据"。
- 网络资源:Stata 官方 Data Management FAQs,覆盖 merge / reshape 几乎所有常见坑。
- 中文实战:中国工业企业数据库匹配指南(杨汝岱 2015);CSMAR 数据中心"上市公司财务指标"代码手册。