前置条件与学习依赖 / PREREQUISITES
① 数学 / 统计基础
描述统计基础(均值 / 中位数 / 分位数)、常见概率分布(正态 / 偏态)、缺失数据机制(MCAR / MAR / MNAR)与诊断原理。
② 经济学理论前置
无专门理论要求;建立“数据质量决定结论上限”的实证意识即可。
③ 软件 / 计算前置
Stata 17+(外部命令:misstablemvpatternswinsor2ipolate;基础 summarizedropmiss)。
④ 站内前置页面
本页为实证流程起点,先浏览 实证总览与路线,无前置方法页。
⑤ 难度分级
入门

01 为什么数据清洗决定论文上限

任何从 CSMAR、Wind、中国工业企业数据库(ASIF)下载下来的原始面板,都不会"干净到可以直接跑 reg"。原始数据里常见的问题包括:同一变量在不同年份编码不一致、上市公司股票代码因更名/退市而变化、财务报表口径在 2007 年新会计准则后发生切换、缺失值被写成空字符串或 -999、极端值由录入错误或并表导致。如果不在这一步把它们处理干净,后面无论用多么精巧的识别策略,结论都站不住。

经济学上一个朴素的直觉:测量误差(measurement error)对 OLS 系数的衰减偏误(attenuation bias),会把系数向零拉。当因变量 $y$ 含有纯测量误差 $\varepsilon$ 时,$\hat\beta$ 仍是无偏的;但当核心解释变量 $x$ 含有测量误差时,估计系数会被系统性地低估:

经典变量误差模型(Classical Measurement Error)
$$x^* = x + u, \quad plim(\hat\beta) = \beta \cdot \frac{\sigma_x^2}{\sigma_x^2 + \sigma_u^2} < \beta$$

也就是说,脏数据不仅让你的结果不显著,还会让你错误地推翻一个本来成立的假设。这就是为什么数据清洗不是"体力活",而是实证识别的一部分。

02 缺失值诊断:misstable 与 mvpatterns

清洗的第一步永远是诊断(diagnose),而不是直接删。你需要先回答三个问题:每个变量缺多少?缺失是不是集中在某几个变量上?缺失在公司-年份上的分布是什么样?Stata 里有两个命令专门做这件事:misstable 给出每个变量的缺失计数与占比;mvpatterns 把"哪几个变量同时缺失"的模式列出来。

Step 1 · 读入数据,先用 sysuse 演示
下面这段代码用 Stata 自带的 nlsw88(1988 年全国青年纵向调查)演示缺失诊断,逻辑可直接套用到你的 CSMAR 面板。
Step 2 · 用 misstable summarize 看连续变量缺失
它会列出每个变量的非缺失样本量、缺失样本量,以及"唯一值个数"——用来识别是否把缺失编码成了 -999 之类的哨兵值。
Step 3 · 用 mvpatterns 看缺失模式
mvpatterns 会把"哪几个变量同时缺失"的组合按频次排序,帮你识别是否有"整家公司整段年份都没数据"的结构性缺失。
Stata · 缺失值诊断
*--------------------------------------------------------------*
*  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 缺失值的 27 种"特殊缺失"

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(非随机缺失):缺失本身与未观测到的信息有关(例如业绩差的公司不愿披露)。这种情况下,任何插补都会引入偏误,应在论文中讨论并做敏感性分析。
插补方法:均值插补 vs. 回归插补
$$\text{Mean imputation: } \hat{x}_{i,miss} = \bar{x}; \qquad \text{Regression imputation: } \hat{x}_{i,miss} = \hat\gamma_0 + \hat\gamma_1 z_i$$

对于公司-年份面板,最常用的"时间序列上的缺失"用 ipolate 做线性插值(linear interpolation):把相邻两个真实值之间的空缺用直线连起来。这在财务指标(如中间年份的 R&D 缺失)上非常常见。

Stata · 缺失值处理三种策略
*--------------------------------------------------------------*
*  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σ 稳健得多。
三种规则的阈值
$$\text{Tukey: } x < Q_1 - 1.5\,IQR \;\lor\; x > Q_3 + 1.5\,IQR$$ $$\text{3}\sigma\text{: } |x - \bar{x}| > 3\,\sigma \qquad \text{MAD: } |x - \tilde{x}| > 3 \cdot 1.4826 \cdot \text{median}(|x_i - \tilde{x}|)$$
Stata · 异常值识别
*--------------------------------------------------------------*
*  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% 缩尾的定义
$$\tilde{x}_i = \max\big(Q_{0.01},\; \min\big(Q_{0.99},\; x_i\big)\big)$$

为什么是 1% 而不是 5%?这是中文顶刊的一个"惯例":

  • 1% 双侧缩尾:最常用,适用于绝大多数财务变量。它只处理极端尾部,保留了分布的主体信息。《经济研究》《管理世界》大多数实证论文默认这个口径。
  • 5% 双侧缩尾:当数据噪声极大(如早期 ASIF、手工采集数据)时使用。代价是把中间 10% 的真实波动也压平了。
  • 1% 单侧(只缩上尾):当变量本身就有下界(如比例类变量、[0,1] 之间)时使用。

Stata 官方没有 winsorize 命令(只有较新版本的 winsor2 是外部命令,最常用)。下面给出两种写法。

Stata · winsorize 缩尾
*--------------------------------------------------------------*
*  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 三件套是标准做法。

Stata · 重复值处理
*--------------------------------------------------------------*
*  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 这种无意义编码,必须重命名为 roelev 这种可读名字。

Stata · 重命名与批量操作
* 单个重命名
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。

Stata · 类型转换
*--------------------------------------------------------------*
*  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 全部写好,后续 esttaboutreg2 导出的表格会自动用中文标签,省去后期在 Word 里手改表格的痛苦。

08 对数变换与变量构造

经济学中大量变量是右偏(right-skewed)的——企业规模、资产、销售额、工资都呈对数正态分布。直接放进 OLS 会被少数巨头主导。标准做法是做对数变换:

对数变换与"半弹性"
$$\ln(y) = \alpha + \beta \ln(x) + \gamma Z + \varepsilon \;\Rightarrow\; \beta \text{ 是弹性}$$ $$\ln(y) = \alpha + \beta\,x + \gamma Z + \varepsilon \;\Rightarrow\; 1 \text{ 单位 } x \text{ 变化约带来 } 100\beta\% \text{ 的 } y \text{ 变化}$$

注意:对数不能取非正数。财务数据里经常出现"现金流为负"或"研发支出为 0",标准处理是 ln(x+1)ln(max(x, 0) + 1),后者在中文论文里被称为 log(1+x) 变换

Stata · 对数变换与常用变量构造
*--------------------------------------------------------------*
*  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 论文案例:数据清洗在顶刊中的角色

English · 经典
The Dynamics of Productivity in the Telecommunications Equipment Industry
Sanghamitra Das, Mark Roberts & James Tybout · Econometrica · 1996
用智利制造业面板估计企业生产率,数据清洗部分(剔除负增加值、处理产能利用率、价格平减)直接决定了 Olley-Pakes 估计量的可信度。展示了"数据清洗不是附属工作,而是识别策略的一部分"。
English · 经典
The Growth of Low-Skill Service Jobs and the Polarization of the U.S. Labor Market
David Autor & David Dorn · American Economic Review · 2013
用美国人口普查(Census)与 CPS 数据构造 1950–2005 年的职业-地区面板。其附录中关于"职业代码跨年代对齐(OCG-1950 → OCC-2000)"的代码是数据清洗的范本——本质上就是本章讲的"行业/职业代码匹配 + 重复值处理"。
中文顶刊 · 标杆
中国企业对外直接投资是否促进了企业创新
毛其淋、许家云 · 《世界经济》 · 2014 年第 8 期
用中国工业企业数据库 + 境外投资企业名录匹配样本。文中明确报告了剔除"工业总产值、增加值、从业人员、固定资产合计为负或缺失"的观测、按 1% 双侧缩尾的标准流程,是中文实证论文数据清洗章节的模板写法。
中文顶刊 · 标杆
《分析师跟踪、利益冲突与股价同步性》
朱红军、何贤杰、陶川 · 《经济研究》 · 2007 年增刊(及后续朱红军团队系列)
基于 CSMAR 的公司-年份面板。其数据处理部分演示了:剔除金融保险业、剔除 ST/*ST、剔除资不抵债、按 1% 双侧缩尾、对连续变量做对数变换——这几乎是当前《经济研究》《管理世界》实证论文的"标准操作清单"。

10 常见错误与调试

错误 1:忘记 Stata 缺失值"大于任何数"

gen pos = price > 0 会把所有缺失值也算进 pos == 1。正确写法:gen pos = price > 0 if !missing(price)。调试办法:count if missing(price),再对照 count if price > 0 的差异。

错误 2:在缩尾之前删样本,或在缩尾之后删样本

两种顺序都会让分位数不一致。标准顺序:①确定分析样本(剔除不该出现的观测)→ ②在该样本上算分位数并缩尾 → ③做回归。把每一步的 count 写在 log 里,方便审稿人复现。

错误 3:winsor2 不指定 by(year) 跨年度混在一起缩

公司-年份面板里,2007 年与 2020 年的价格水平完全不同。如果不按年份分组缩尾,会用 14 年混合分布的分位数去压每一年,把早期小公司误判成"异常值"。正确写法:winsor2 roe, cuts(1 99) by(year)

错误 4:用 listwise deletion 静默丢样本

Stata 的 regress y x1 x2 会自动丢掉任一变量缺失的观测。新手上常在不知道的情况下损失了 30% 的样本。解决:在每个回归前手动 gen sample = !missing(y,x1,x2)tab sample 报告样本量变化。

11 进阶学习资料

  • Stata 官方help misstablehelp mvpatternshelp ipolatehelp 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,是数据清洗规范的金标准。