数据清洗:从原始数据到分析样本
缺失值诊断与处理、异常值识别(箱线图 / 3σ / MAD)、winsorize 缩尾、重复值处理、变量类型转换与标签系统——这是 Table 1 之前的全部工作。
misstable、mvpatterns、winsor2、ipolate;基础 summarize、dropmiss)。01 为什么数据清洗决定论文上限
任何从 CSMAR、Wind、中国工业企业数据库(ASIF)下载下来的原始面板,都不会"干净到可以直接跑 reg"。原始数据里常见的问题包括:同一变量在不同年份编码不一致、上市公司股票代码因更名/退市而变化、财务报表口径在 2007 年新会计准则后发生切换、缺失值被写成空字符串或 -999、极端值由录入错误或并表导致。如果不在这一步把它们处理干净,后面无论用多么精巧的识别策略,结论都站不住。
经济学上一个朴素的直觉:测量误差(measurement error)对 OLS 系数的衰减偏误(attenuation bias),会把系数向零拉。当因变量 $y$ 含有纯测量误差 $\varepsilon$ 时,$\hat\beta$ 仍是无偏的;但当核心解释变量 $x$ 含有测量误差时,估计系数会被系统性地低估:
也就是说,脏数据不仅让你的结果不显著,还会让你错误地推翻一个本来成立的假设。这就是为什么数据清洗不是"体力活",而是实证识别的一部分。
02 缺失值诊断:misstable 与 mvpatterns
清洗的第一步永远是诊断(diagnose),而不是直接删。你需要先回答三个问题:每个变量缺多少?缺失是不是集中在某几个变量上?缺失在公司-年份上的分布是什么样?Stata 里有两个命令专门做这件事:misstable 给出每个变量的缺失计数与占比;mvpatterns 把"哪几个变量同时缺失"的模式列出来。
nlsw88(1988 年全国青年纵向调查)演示缺失诊断,逻辑可直接套用到你的 CSMAR 面板。*--------------------------------------------------------------*
* 02-missing-diagnose.do —— 缺失值诊断
*--------------------------------------------------------------*
clear all
set more off
* 用 Stata 自带的 nlsw88 演示(女性工资面板,含不少缺失)
sysuse nlsw88, clear
* 1) misstable summarize:看连续变量的缺失计数与占比
* 输出 Unabbrev. / Obs. / Unique / Mean / Min / Max
misstable summarize
* 2) misstable patterns:按"哪几个变量同时缺失"分组统计
* 这一步能告诉你缺失是不是结构性的(成批缺失)
misstable patterns wage tenure ttl_exp collgrad
* 3) mvpatterns:等价命令,但输出更紧凑
mvpatterns wage tenure ttl_exp
* 4) 关键检查:是否把缺失写成了哨兵值(如 -999, 9999, 空字符串)
* 对数值变量,看最小值是否异常地小
summarize wage tenure ttl_exp, detail
* 5) 把"伪缺失"替换成真正的 .(Stata 缺失值)
* 假设原始数据里 -999 代表缺失
* replace wage = . if wage == -999
* replace tenure = . if tenure == -999
Stata 的数值缺失不仅是 .,还有 .a .b ... .z 共 27 个,并且它们在数值上"大于任何真实数"(即 . > 1e300)。这意味着 sum price if price > 0 会把所有缺失值也包含进来!正确写法是 sum price if price > 0 & !missing(price)。
03 缺失值处理:drop / impute / 插值
诊断清楚之后,处理缺失值主要有三条路。它们不是"三选一",而是根据缺失机制来选:
- MCAR(完全随机缺失):缺失与任何变量都无关。这种情况下,直接 drop 损失最小。
- MAR(条件随机缺失):缺失只与观测到的协变量有关。可以用分组均值/中位数插补,或多重插补。
- MNAR(非随机缺失):缺失本身与未观测到的信息有关(例如业绩差的公司不愿披露)。这种情况下,任何插补都会引入偏误,应在论文中讨论并做敏感性分析。
对于公司-年份面板,最常用的"时间序列上的缺失"用 ipolate 做线性插值(linear interpolation):把相邻两个真实值之间的空缺用直线连起来。这在财务指标(如中间年份的 R&D 缺失)上非常常见。
*--------------------------------------------------------------*
* 03-missing-handling.do —— drop / impute / ipolate
*--------------------------------------------------------------*
clear all
set more off
* 先用 sysuse auto 构造一个含缺失的迷你面板
sysuse auto, clear
keep make price mpg weight
gen year = 1978
* 人为制造一些缺失(演示用)
replace price = . if _n == 5
replace price = . if _n == 10
replace mpg = . if _n == 3
* ---------- 策略 A:listwise deletion(整行删除) ----------
* 只要回归所需任一变量缺失,就丢掉这一行。
* 注意:regress 命令默认就做这件事(listwise deletion)。
* 显式写法:
preserve
drop if missing(price, mpg, weight)
count // 看一下删完还剩多少
restore
* ---------- 策略 B:分组均值 / 中位数插补 ----------
* 按 foreign(是否进口车)分组,用组内中位数填补 price 的缺失
* 注意:插补会人为压缩方差,建议只在缺失很少时用
bysort foreign: egen price_med = median(price)
gen price_imp = price
replace price_imp = price_med if missing(price)
* ---------- 策略 C:ipolate 线性插值(面板内时间插值) ----------
* ipolate 适合"同一公司在时间序列上偶尔缺一年"的场景。
* 下面用一个手工构造的公司-年份序列演示:
clear
set obs 10
gen id = 1
gen year = 2010 + _n
gen roe = .
replace roe = 0.05 in 1
replace roe = 0.08 in 4
replace roe = 0.06 in 8
* 在同一 id 内部,按 year 排序后插值
bysort id (year): ipolate roe year, gen(roe_ipol)
list year roe roe_ipol
* ---------- 策略 D:多重插补(MICE 的简单版) ----------
* Stata 原生 mi 命令族;缺失比例大时更稳健。
* mi set flong
* mi register imputed wage tenure ttl_exp
* mi impute chained (regress) wage tenure = collgrad smsa, add(5)
插补得到的 price_imp 是人造数据,它会人为减小样本方差、夸大系数显著性。论文里应当:(1) 报告原始样本与插补后样本的对比;(2) 主回归用"干净样本",稳健性检验里用插补样本。
04 异常值识别:箱线图 / 3σ / MAD
异常值(outlier)指那些与主体数据分布严重偏离的观测。它们可能是真实的极端事件(如某公司当年利润暴增 100 倍),也可能是录入错误。识别异常值常用三种规则:
- 箱线图法则(Tukey's fences):低于 $Q_1 - 1.5 \cdot IQR$ 或高于 $Q_3 + 1.5 \cdot IQR$ 即视为异常。IQR = $Q_3 - Q_1$。
- 3σ 法则:偏离均值 $\bar x$ 超过 $3\sigma$ 即视为异常。前提是数据近似正态,对长尾的财务数据不太适用。
- MAD(Median Absolute Deviation,中位数绝对偏差):$MAD = 1.4826 \cdot \text{median}(|x_i - \tilde{x}|)$,对厚尾分布比 3σ 稳健得多。
*--------------------------------------------------------------*
* 04-outlier-detect.do
*--------------------------------------------------------------*
sysuse auto, clear
* ---- 方法 1:箱线图法(IQR) ----
* 用 summarize, detail 拿四分位数
summarize price, detail
scalar q1 = r(p25)
scalar q3 = r(p75)
scalar iqr = q3 - q1
scalar lo_box = q1 - 1.5*iqr
scalar hi_box = q3 + 1.5*iqr
display "箱线图下界 = " lo_box " 上界 = " hi_box
* 标记异常值
gen outlier_box = (price < lo_box | price > hi_box) if !missing(price)
tab outlier_box
* ---- 方法 2:3σ 法则 ----
summarize price
scalar mu = r(mean)
scalar sd = r(sd)
gen outlier_3sd = (abs(price - mu) > 3*sd) if !missing(price)
tab outlier_3sd
* ---- 方法 3:MAD(中位数绝对偏差,更稳健) ----
* 1.4826 是让 MAD 在正态下等价于标准差的常数
quietly summarize price, detail
scalar med = r(p50)
gen abs_dev = abs(price - med)
quietly summarize abs_dev, detail
scalar mad = 1.4826 * r(p50)
gen outlier_mad = (abs(price - med) > 3*mad) if !missing(price)
tab outlier_mad
* ---- 画箱线图直观检查 ----
graph box price, over(foreign) ///
title("Price 箱线图(按是否进口车分组)") ///
ytitle("Price (USD)")
graph export "output/boxplot_price.png", replace width(1600)
05 winsorize 缩尾:1% 还是 5%
识别出异常值之后,不要直接 drop——那会损失信息。更常见的做法是 winsorize(缩尾):把超过分位点的观测"压"到分位点上,而不是删掉。比如 1% 双侧缩尾,就是把小于 1% 分位点的值都替换成 1% 分位点、把大于 99% 分位点的值都替换成 99% 分位点。
为什么是 1% 而不是 5%?这是中文顶刊的一个"惯例":
- 1% 双侧缩尾:最常用,适用于绝大多数财务变量。它只处理极端尾部,保留了分布的主体信息。《经济研究》《管理世界》大多数实证论文默认这个口径。
- 5% 双侧缩尾:当数据噪声极大(如早期 ASIF、手工采集数据)时使用。代价是把中间 10% 的真实波动也压平了。
- 1% 单侧(只缩上尾):当变量本身就有下界(如比例类变量、[0,1] 之间)时使用。
Stata 官方没有 winsorize 命令(只有较新版本的 winsor2 是外部命令,最常用)。下面给出两种写法。
*--------------------------------------------------------------*
* 05-winsorize.do —— 缩尾处理
*--------------------------------------------------------------*
ssc install winsor2, replace // 首次运行时安装
sysuse auto, clear
* ---- 写法 A:winsor2(推荐,外部命令)----
* 1% 双侧缩尾,生成新变量 price_w1
winsor2 price, cuts(1 99) suffix(_w1)
* 5% 双侧缩尾
winsor2 price, cuts(5 95) suffix(_w5)
* 同时对多个变量缩尾
winsor2 price mpg weight length, cuts(1 99) suffix(_w1)
* 按年份分组缩尾(面板里非常重要!每年单独算分位数)
* winsor2 roe lev size, cuts(1 99) suffix(_w1) by(year)
* 对比缩尾前后
summarize price price_w1 price_w5
* ---- 写法 B:纯手工(不依赖外部命令) ----
* 1% 双侧缩尾
quietly summarize price, detail
scalar lo = r(p1)
scalar hi = r(p99)
gen price_manual_w1 = price
replace price_manual_w1 = lo if price < lo & !missing(price)
replace price_manual_w1 = hi if price > hi & !missing(price)
常见错误是"先缩尾、后筛样本"。正确顺序是先确定最终分析样本(剔除金融类 ST、剔除资不抵债、保留 2007 年后),再在这个最终样本上算分位数并缩尾。否则你会用一个不断变化的样本算分位数,结果不可复现。
06 重复值处理与变量重命名
原始下载数据经常包含重复观测——同一公司同一年出现两行(可能是数据库不同报表版本、合并报表与母公司报表重复)。Stata 里 duplicates report/list/tag 三件套是标准做法。
*--------------------------------------------------------------*
* 06-duplicates.do —— 重复值处理
*--------------------------------------------------------------*
sysuse nlsw88, clear
* 1) 报告按 idcode(员工 ID)的重复情况
duplicates report idcode
* 2) 列出重复的具体观测
duplicates list idcode in 1/50
* 3) 给每个重复组打标签
duplicates tag idcode, gen(dup_flag)
tab dup_flag // 0 表示唯一,>0 表示重复
* 4) 标准做法:保留每组中最新的一条
* (按某个"优先级变量"排序后 keep 第一条)
* 假设我们有一个报表版本号 vers,数值大的更新
* gsort idcode -vers
* by idcode: keep if _n == 1
* duplicates report idcode // 再验证一次
* 5) 如果只是完全重复的行(所有变量都一样),直接 drop
duplicates drop
变量重命名:rename 与 rename group
从 CSMAR 导出的变量名常常是 A001001000 这种无意义编码,必须重命名为 roe、lev 这种可读名字。
* 单个重命名
rename A001001000 roe
rename B002003000 lev
* 批量重命名:把所有 year 前缀统一
rename (yr_*) (year_*) // yr_a -> year_a
* 批量:去掉下划线统一小写
rename *, lower
07 数据类型转换与标签系统
Stata 的变量类型分三类:byte/int/long/float/double(数值型)、str1-str244(字符串)、以及带 value label 的分类变量。CSMAR 导出的行业代码常常是字符串"行业代码 1 位 + 4 位数字",需要先 destring。
*--------------------------------------------------------------*
* 07-type-convert.do
*--------------------------------------------------------------*
* 字符串 -> 数值(去掉非数字字符后转)
* destring industry, replace ignore(" ")
* 数值 -> 字符串
tostring year, replace
* 把"四位行业代码"截成"一位大类"(证监会行业分类)
gen industry1 = real(substr(industry_code, 1, 1))
* 把数值型 id 保留前导零:股票代码在 Stata 里常被吞掉前导 0
gen stkcd_str = string(stkcd, "%06.0f") // 1 -> "000001"
* ---------- 标签系统:label variable / label define ----------
* 这是让 esttab 导出的三线表"看得懂"的关键
sysuse auto, clear
* 1) 变量标签(出现在 describe / 输出表格里)
label variable price "汽车价格 (USD)"
label variable mpg "每加仑英里数"
label variable weight "整车重量 (lbs)"
label variable foreign "是否进口车"
* 2) 值标签(把 0/1 翻译成文字)
label define foreign_lbl 0 "本土" 1 "进口"
label values foreign foreign_lbl
* 3) 看效果
describe
tab foreign
只要在清洗阶段把 label variable 全部写好,后续 esttab、outreg2 导出的表格会自动用中文标签,省去后期在 Word 里手改表格的痛苦。
08 对数变换与变量构造
经济学中大量变量是右偏(right-skewed)的——企业规模、资产、销售额、工资都呈对数正态分布。直接放进 OLS 会被少数巨头主导。标准做法是做对数变换:
注意:对数不能取非正数。财务数据里经常出现"现金流为负"或"研发支出为 0",标准处理是 ln(x+1) 或 ln(max(x, 0) + 1),后者在中文论文里被称为 log(1+x) 变换。
*--------------------------------------------------------------*
* 08-log-transform.do
*--------------------------------------------------------------*
sysuse auto, clear
* 1) 标准对数变换(要求 price > 0)
gen ln_price = ln(price)
label variable ln_price "ln(价格)"
* 2) 含零/负值时的"稳健对数":ln(1+x)
* 常用于 R&D、广告支出这类常有 0 的变量
gen rd = runiform()*100 // 模拟 R&D 支出
gen ln_rd = ln(rd + 1)
label variable ln_rd "ln(1+R&D)"
* 3) 财务实证中常见的核心变量构造(CSMAR 口径)
* gen size = ln(total_assets) // 企业规模
* gen lev = total_liab / total_assets // 资产负债率
* gen roa = net_profit / total_assets // 总资产收益率
* gen roe = net_profit / total_equity // 净资产收益率
* gen tbq = (market_cap + total_liab) / total_assets // 托宾 Q
* gen soe = (state_owned_share >= 0.3) // 国企虚拟变量
* 4) 描述一下看分布是否对称
summarize price ln_price, detail
09 论文案例:数据清洗在顶刊中的角色
10 常见错误与调试
写 gen pos = price > 0 会把所有缺失值也算进 pos == 1。正确写法:gen pos = price > 0 if !missing(price)。调试办法:count if missing(price),再对照 count if price > 0 的差异。
两种顺序都会让分位数不一致。标准顺序:①确定分析样本(剔除不该出现的观测)→ ②在该样本上算分位数并缩尾 → ③做回归。把每一步的 count 写在 log 里,方便审稿人复现。
公司-年份面板里,2007 年与 2020 年的价格水平完全不同。如果不按年份分组缩尾,会用 14 年混合分布的分位数去压每一年,把早期小公司误判成"异常值"。正确写法:winsor2 roe, cuts(1 99) by(year)。
Stata 的 regress y x1 x2 会自动丢掉任一变量缺失的观测。新手上常在不知道的情况下损失了 30% 的样本。解决:在每个回归前手动 gen sample = !missing(y,x1,x2),tab sample 报告样本量变化。
11 进阶学习资料
- Stata 官方:
help misstable、help mvpatterns、help ipolate、help duplicates,每条命令的 PDF 手册都有完整例子。 - 外部命令文档:winsor2 的作者说明在 Stata Journal 文章(同一作者 also 写了 winsor2 的论文)。
- 教材:陈强《高级计量经济学及 Stata 应用》第 2 章"数据处理";Cameron & Trivedi, Microeconometrics Using Stata(Rev. ed.)第 3 章。
- 中国数据手册:杨汝岱(2015)关于中国工业企业数据库的清理指南(《经济学季刊》);CSMAR 官方"研究数据库使用指南"PDF。
- 课程:世界银行 DME / DIME Wiki 的 Stata Coding Practices,是数据清洗规范的金标准。