EDA 探索性分析与样本构造 Exploratory Data Analysis
在跑任何一个 reg 之前,你必须先"用眼睛看一遍数据"。直方图告诉你变量偏不偏、散点图告诉你关系是不是线性、时间趋势图告诉你有没有结构断点、分组均值图告诉你组间差多少。这一页把 EDA 的全套图、样本 flow chart、样本选择诊断串成一套可照抄的 Stata 流程。
01 EDA 的定位:跑回归之前必须做的事
很多初学者拿到数据的第一反应是 reg y x controls,然后看 p 值。这是一种危险的习惯。回归是对数据的一种"压缩"——它把成千上万行数据压缩成一个系数、一个标准误、一个 p 值。如果你没先看过数据长什么样,你根本不知道这个压缩过程丢掉了什么。
EDA(Exploratory Data Analysis,探索性数据分析)就是"在跑回归之前,先用眼睛把数据看一遍"。它不是正式的假设检验,而是对数据的直觉训练:Y 的分布偏不偏?X 和 Y 是线性关系还是 U 型?处理组和对照组在政策前有没有平行趋势?异常值是真实的极端事件还是录入错误?这些问题,回归表回答不了,图能回答。
EDA 的另一个重要功能是防止你被回归系数骗。Anscombe (1973) 著名的四重奏(Anscombe's quartet)展示了四组完全不同的散点图,它们的均值、方差、相关系数、回归系数竟然完全一样——如果你只看回归表,你会以为四组数据是一回事;如果你看图,你会发现一组是线性、一组是曲线、一组有异常值、一组是杠杆点牵着走。这就是为什么每一篇严肃的实证论文,在主回归之前都要有一组描述性图。
(1) 单变量分布:每个变量长什么样?(2) 双变量关系:X 和 Y 怎么一起动?(3) 分组/时间维度:处理组 vs 对照组、政策前 vs 政策后、不同行业之间的差异。下面三节按这三层展开。
02 分布图:histogram / kdensity / boxplot
2.1 直方图 histogram:看整体形状
直方图把数值范围切成若干等宽的 bin,数每个 bin 里的观测数。它告诉你:分布是单峰还是多峰?是对称还是偏态?有没有一坨数据挤在 0 附近?
- 右偏(right-skewed):长尾拖向右边,绝大多数观测集中在小值,少数极端大值。企业规模、收入、专利数通常右偏——此时用对数变换
gen ly = log(y+1)通常能让分布对称。 - 左偏(left-skewed):长尾拖向左边,少数极端小值。
- 多峰(multimodal):出现两个峰,说明数据可能是两种不同子总体混合的——比如处理组和对照组没分开画,结果叠出双峰。
2.2 核密度 kdensity:看平滑形状
直方图受 bin 宽度影响很大,bin 太粗看不到细节,bin 太细噪声大。kdensity 用核密度估计把直方图平滑成一条连续曲线,更容易和正态分布(normal 选项)对比。如果曲线明显偏离对称钟形,就不要在没变换的变量上跑 OLS——OLS 对偏态不敏感,但假设检验和预测区间会受影响。
2.3 箱线图 boxplot:看异常值
箱线图把中位数、四分位距(IQR)、须线、疑似异常值画成一张图。箱线图是检查异常值最快的工具:任何超过须线(通常是 1.5×IQR)的点都会被单独画出来。你应该逐一看这些异常点——它们是真实的极端事件(如一家企业在政策冲击年产能翻倍),还是数据录入错误(如把"100"写成了"10000")?
异常值在微观数据里往往携带最多信息——它们可能正是你想研究的"极端处理效应"。先诊断它是什么,再决定是缩尾(winsorize)、对数变换、还是保留。winsor2 是把 1% 和 99% 分位点以外的值"压缩"到分位点,而不是删除——这是更稳妥的做法。
03 散点图与拟合线:scatter / lowess / binscatter
3.1 散点图 twoway scatter
两个连续变量的最基本图。N 很大时(>10000),散点会糊成一坨,看不出形状——此时应该用 binscatter。
3.2 lowess 局部拟合
lowess y x 不假设全局线性,而是在每个点附近用局部多项式拟合一条曲线。它让你无参数地看到 X 和 Y 的真实关系形状:是单调上升?倒 U 型?有平台期?如果 lowess 明显弯了,你在线性回归里加一个 c.x##c.x 二次项就不是"数据挖掘",而是"按图索骥"。
3.3 binscatter:大样本的散点替代品
binscatter y x, nquantiles(20) 把 X 分成 20 个等频 bin,每个 bin 画一个点(该 bin 内 X 和 Y 的均值)。它比散点图清爽得多,且能自动加线性拟合线。binscatter 是顶刊论文里最常见的 EDA 图——因为它在大样本下既不糊、又能展示非线性。
* 三件套:散点 + lowess + binscatter
twoway (scatter y x) (lfit y x), name(sc1, replace)
twoway (lowess y x, bw(0.3)), name(sc2, replace)
binscatter y x, nquantiles(20) name(sc3, replace)
* 并排放三张图
graph combine sc1 sc2 sc3, rows(1) ycommon
graph export "output/figures/bivariate_eda.png", replace
04 时间趋势图:tsline 与 by(year) 均值
面板数据的 EDA 必须包括时间维度。它回答两个问题:(1) 结果变量 Y 在时间上是平稳的、有趋势的、还是有结构断点?(2) 处理组和对照组在政策前是否平行?
4.1 整体时间趋势
对年度数据:collapse (mean) y, by(year),然后 tsline y 或 line y year。一眼能看出:政策实施年份前后有没有跳跃?有没有持续趋势?有没有异常年份?
4.2 分组时间趋势(DID 平行趋势预检验)
这是 DID 识别策略的命脉。按处理组和对照组分别画 Y 的年均值趋势:
* 按 (year, treat) 计算 Y 的均值
collapse (mean) y, by(year treat)
twoway (line y year if treat==1) ///
(line y year if treat==0), ///
legend(label(1 "处理组") label(2 "对照组")) ///
xline(2012, lpattern(dash) lcolor(red)) /// // 政策实施年份
title("政策前后两组均值趋势")
* 如果政策前两组基本平行,DID 平行趋势假设看起来合理
* 如果政策前两组已经在发散,DID 就有问题
时间趋势图还能帮你发现结构断点:2008 年金融危机前后、2015 年汇改前后、疫情前后——这些外生冲击会让你的回归系数跳变,必须在模型里控制时间固定效应或剔除异常年份。
05 分组均值图:条形图与置信区间
回归系数本质上是"组间均值差"。在跑回归之前,你应该先用最简单的图把这个差异画出来:处理组和对照组的 Y 均值差多少?95% 置信区间有没有重叠?
- 条形图(bar chart):
graph bar (mean) y, over(treat)画两组均值的高度对比。 - 点估计 + 置信区间图:
coefplot或ciplot直接把均值差和 95% CI 画成横向点线图。这比数字表直观得多。
这一步的价值在于:在加任何控制变量之前,先看"裸"的组间差是什么样。如果处理组和对照组在 Y 上裸差就很小,你的回归系数再显著也解释不出什么经济意义;如果裸差很大但回归系数不显著,那是控制变量或样本选择在起作用,需要回去查。
06 样本构造流程与 flow chart
样本构造是 EDA 里最容易被忽略、但审稿人最关心的部分。你必须能回答:从原始观测到最终回归样本,每一步删了多少观测?为什么删?
count 记下 N0。keep if industry=="M" & year>=2008,记下 N1。drop if missing(Y, X),记下 N2。winsor2),而不是直接删除——缩尾不减少 N,只改极端值。把 N0 → N1 → N2 → N3 做成一张 flow chart(横向条形图或表格)放在论文附录里。审稿人一看就知道你的样本是怎么来的,也知道你每一步删了谁。flow chart 的另一个隐性作用是:它逼你把"为什么删"写成一句话——如果你写不出"为什么保留制造业",说明你的样本选择本身就没想清楚。
07 样本选择检查:谁被删了?
最危险的情况是:你删掉的观测,恰恰是和 Y 最相关的那批。例如你因为"研发支出缺失"删企业,如果"研发支出缺失"的企业正是创新失败、最该被研究的企业,那你删掉的就是最有信息量的观测。
诊断方法很简单:把"被删"和"被留"两组在关键变量上做均值检验。
* 假设 kept==1 是保留下来的样本
gen byte kept = !missing(Y, X, controls)
* 对比被删组 vs 保留组在 X、Y、控制变量上的均值
foreach v in X Y age size leverage {
quietly ttest `v', by(kept)
display "变量 `v': 被删组均值=" r(mu_1) " 保留组均值=" r(mu_2) ///
" p值=" r(p)
}
* 如果 p 值很小,说明"能否进样本"和这些变量系统相关
* 你的 Sample 层有选择偏误,需要在论文里讨论
如果"被删"和"被留"在 Y 上均值差异显著,你的回归结果不能解释为对原始总体的效应——它只能解释为对"被保留下来的子总体"的效应。这正是上一页"五层框架"里 Sample 层的核心警告。
08 完整 Stata 代码:模拟数据全套 EDA
下面这段代码自己生成一份模拟面板数据(1000 家企业 × 10 年),然后跑完整套 EDA。你可以直接复制到 Stata 里跑通,作为你自己项目的模板。
*==============================================================
* 14_eda_full.do:模拟数据的全套 EDA
* 目标:在跑任何回归之前,先用眼睛看一遍数据
*==============================================================
clear all
set more off
set seed 20260911
* --- 1. 生成模拟面板数据 ---
* 1000 家企业 × 10 年 = 10000 观测
set obs 1000
gen firm_id = _n
gen treat = (runiform() < 0.5) // 处理组 50%
expand 10
bysort firm_id: gen year = 2010 + _n - 1
* 处理变量:2015 年及以后处理组受到政策冲击
gen post = (year >= 2015)
gen did = treat * post
* 协变量:企业规模(对数正态)、年龄
gen size = exp(rnormal(2, 1))
gen age = floor(runiform()*20 + 1)
* 结果变量:企业利润(对数)
* 真实模型:lprofit = 0.5*did + 0.3*ln(size) - 0.01*age + 噪声
gen ln_size = log(size)
gen lprofit = 0.5*did + 0.3*ln_size - 0.01*age + rnormal(0, 1)
* 故意加几个异常值(1% 的极端大值)
replace lprofit = lprofit + 8 if runiform() < 0.01
* 故意让部分企业在 2010 年缺失利润(模拟选择偏误)
replace lprofit = . if year==2010 & treat==1 & runiform()<0.3
*==============================================================
* 2. 单变量分布 EDA
*==============================================================
* 2.1 直方图 + 正态叠加
histogram lprofit, normal name(h_lp, replace) ///
title("利润对数分布")
graph export "eda_1_hist_lprofit.png", replace
* 2.2 核密度(处理组 vs 对照组)
twoway (kdensity lprofit if treat==1) ///
(kdensity lprofit if treat==0), ///
legend(label(1 "处理组") label(2 "对照组")) ///
name(kd_grp, replace)
graph export "eda_2_kdensity_group.png", replace
* 2.3 箱线图:按处理组
graph box lprofit, over(treat) name(box_grp, replace)
graph export "eda_3_boxplot.png", replace
*==============================================================
* 3. 双变量关系 EDA
*==============================================================
* 3.1 binscatter:ln(size) vs lprofit
binscatter lprofit ln_size, nquantiles(30) ///
name(bin_size, replace) ///
title("企业规模 vs 利润")
graph export "eda_4_binscatter_size.png", replace
* 3.2 lowess:年龄 vs lprofit(看是否非线性)
lowess lprofit age, bw(0.3) name(lowess_age, replace)
graph export "eda_5_lowess_age.png", replace
*==============================================================
* 4. 时间趋势 EDA(DID 平行趋势预检)
*==============================================================
preserve
collapse (mean) lprofit, by(year treat)
twoway (line lprofit year if treat==1) ///
(line lprofit year if treat==0), ///
legend(label(1 "处理组") label(2 "对照组")) ///
xline(2015, lpattern(dash) lcolor(red)) ///
name(trend, replace) ///
title("政策前后利润趋势(2015 为政策年)")
graph export "eda_6_trend.png", replace
restore
*==============================================================
* 5. 分组均值 + 置信区间
*==============================================================
* 2015 年前 vs 后,处理组 vs 对照组
preserve
keep if !missing(lprofit)
ci means lprofit, by(treat post)
restore
*==============================================================
* 6. 样本构造 flow chart
*==============================================================
count
scalar N0 = r(N)
display "Step0 原始观测: " N0
* 只保留 2010 年以后(其实数据就是 2010 起)
keep if year >= 2010
count
scalar N1 = r(N)
display "Step1 保留2010年后: " N1
* 删除 lprofit 缺失
drop if missing(lprofit)
count
scalar N2 = r(N)
display "Step2 删除lprofit缺失: " N2
* 缩尾 1%/99%(注意:缩尾不改变 N)
capture which winsor2
if _rc ssc install winsor2, replace
winsor2 lprofit ln_size, cuts(1 99) replace
count
scalar N3 = r(N)
display "Step3 缩尾后: " N3 "(N 不变)"
display _n "===== Flow Chart ====="
display "N0 = " N0
display "N1 = " N1 " (删了 " N0-N1 ")"
display "N2 = " N2 " (删了 " N1-N2 ")"
display "N3 = " N3 " (最终回归 N)"
*==============================================================
* 7. 样本选择诊断:被删的是谁?
*==============================================================
* 重新读一遍原始数据,标记哪些被删了
preserve
* 这里略:实际项目中用 kept 哑变量 + ttest 对比
display "请参考本页第 07 节的 ttest 模板"
restore
*==============================================================
* 8. 最后才跑回归
*==============================================================
reg lprofit did ln_size i.age, vce(cluster firm_id)
estimates table, b(%9.3f) se(%9.3f) stats(N r2)
display _n "===== EDA 全部完成 ====="
display "先看图,再看回归表。两者必须对得上。"
09 常见错误
错误 1:只看回归表,不看分布图
这是最普遍的错误。reg y x 跑出来 p=0.03,你就宣布效应显著。但如果你先画一张 lprofit 的直方图,你会发现它右偏严重、1% 的极端值把 OLS 拉到一边。这种情况下,回归系数显著不等于"经济关系显著"——它可能只是被几个极端点拖着走。每一个核心变量,在跑回归之前都要画一张直方图或核密度图。
错误 2:忽略样本选择
直接在"有完整数据"的子样本上跑回归,不检查被删掉的观测是不是随机的。后果是:你估计的不是总体效应,而是"能被观测到的那部分"的效应。下一页五层框架会反复强调这一点。
错误 3:异常值不诊断,直接缩尾或删除
看到箱线图上有几个点飘在外面,winsor2, cuts(1 99) 一缩了事。但那几个点可能是真实的政策受益者——比如一家企业因为政策产能翻了三倍。在不诊断的情况下缩尾,等于把你最该研究的观测"压扁"了。正确顺序:先 list 看这几个点是谁、为什么这么极端,再决定缩尾还是保留。
错误 4:不画时间趋势就跑 DID
DID 的平行趋势假设是"处理组和对照组在政策前趋势平行"。这个假设在回归里检验不了(事件研究图可以部分检验),但在 EDA 阶段,一张简单的分组时间趋势图就能让你看到平行趋势是否成立。如果政策前两组线已经在发散,你跑出来的 DID 系数再显著也没用。
错误 5:binscatter 没加控制变量就下结论
binscatter 画的是原始 X 和 Y 的关系。如果你加了企业 FE、年份 FE,原始 binscatter 看到的"关系"和回归里的系数完全是两回事。正确做法:在 binscatter 里也加 absorb(firm year) 选项,让图和回归对应。
错误 6:EDA 一次性做完、之后再也不回头看
EDA 不是"开题时做一次就归档"的仪式,而是贯穿整个实证过程的习惯。每次你换了一个样本、加了一个变量、改了一个筛选条件,都应该重画几张关键图:样本 flow chart 更新没有?新加入的变量分布合理吗?被新规则删掉的观测和原来的样本系统不同吗?把 EDA 当成"每次改代码后必跑的自检",而不是"开学第一周的任务"。
回归是把数据"压缩"成一个数;EDA 是在压缩之前,先看清楚你压缩的是什么。先看图,再看表;图和表对不上,一定是表错了,不是图错了。