查看原文
其他

万字长文干货!社会科学实证代码和数据的那些事(一)

Alicia 学说平台 2023-02-24

会议

直播

论文

热点

视频


文|Alicia 编辑|Via



实证社会科学意味着什么呢?


问出好的问题,挖掘新的数据,设计数据分析,写出所得结果。


此过程需投入大量时间在写作及代码调试中,通过编写代码来清理、整理、爬取与合并数据。用代码去执行数据分析、拟合模型、格式化结果并生成图表。


现在虽然有很多人靠写代码谋生,但有一些经济学家、政治学家、心理学家、社会学家,或其他实证研究者都没有受到过任何计算机科学相关的专业培训。他们中的大部分人只是初期简单地学习了基础编程,此后便再也没有下文了。如果说他们应该花更多时间思考写代码的方式,就像告诉一个小说家她应该花更多时间思考如何更好地使用 Microsoft Word。


在编程中,这种自学的、凭经验的方式已经面临瓶颈时,可能会遇到以下状况:


✦ 尝试复制一篇论文早期草稿版本的结果时发现,以前用来得到结果的代码无法运行了,因为它调用的文件已经被移动了位置。当找到之前用到的文件,运行代码后,得到的结果又与先前不同了。

✦ 在项目中期时发现,其中一个回归的观测量出乎意料的小。在检查良久后发现许多观测值在合并中要用到的标识符缺失,因而他们都在合并中被删除了。当纠正了这个错误并保留被错误删除的观测时,结果已经完全改变了。


这些问题在做项目时或多或少是不可以避免的。当项目变得越来越庞大,面临的问题也就越严峻,通过让RA 写指导手册和条款指南、写大量的备注笔记和文档等等,都被证实了始终是没有效率的。


来自斯坦福大学的教授Matthew Gentzkow和哈佛大学的教授Jesse M. Shapiro 根据自己的实证研究经验,针对研处理数据、写代码、论文合作等过程中遇到的问题,撰写了文章“Code and Data for the Social Sciences: A Practitioner’s Guide”,阐述如何将专业人士在代码与数据中的洞见,转化入实证社会科学家的实际操作中。


Matthew Gentzkow

斯坦福大学经济学教授



Jesse M. Shapiro

哈佛大学经济与工商管理教授




Part 1

自动化 (Automation)


规则

(A) 自动化所有可以被自动化的东西

(B) 编写一个可以单独从头到尾执行所有代码的脚本


让我们从一个简单的研究项目开始说起。若想检验将电视引入美国会增加薯条销量的假说。在邮件中,我们收到了一个 Excel 文件,内含两个工作表:


(i) “tv”,包含了美国每个县初次引入电视的年份;

(ii) “chips”,涵盖了从 1940 年到 1970 年每个县薯片总销量的年度数据。


我们想要用薯条销量的对数在是否引入电视的虚拟变量及地区年份固定效应上跑面板回归。


下面是我们可能的操作步骤:


◆ 在 Excel 中打开文件,用“另存为”将工作表保存为 text 文件

◆ 打开数据分析软件,如 Stata,用合适的命令去导入、重整和合并这些 text 文件

◆ 定义一个新变量以衡量 log(chip sales),接下来跑回归

◆ 打开一个新的 MS Word 文件,将软件中的输出结果复制入表格中

◆ 写下并保存关于结果的激动人心的讨论

◆ 投稿期刊


就像所有人在研究生阶段所学的那样,这种“交互式”的研究模式是很糟糕的。数据组建及分析的过程应保存在脚本中,如 Stata 中的 .do 文件、Matlab 中的 .m 文件、R 语言中的 .r 文件等等。


我们需要停下来想想为什么我们不喜欢这种交互的模式,主要有以下两种原因。


1. 可复制性。如果未来的某天或者某一年,想重现上述薯条销量对电视的回归,那我们需要又打开 tv.csv 和 chip.csv 文件,把它们导入到 Stata 中,再继续整理合并和定义变量等等。因为这个分析很简单,可能会幸运地发现得到了同样的回归结果。当然,也可能得不到同样的结果。即使在这个简单的例子中,依然有不计其数的细节可能出错,比如在写论文的过程中会收到 tv.csv 的更新版本,我们可能会不注意用了新的而非旧版,忘记剔除一些薯条销售数据过高的县,计算了常规标准误而非稳健标准误等等。


进一步说,由于对之前的操作没有一个精确的步骤操作记录,因此没有人可以保证论文中的数字是如何得到的。如果有人继续问,为什么表格中报告的观测量与原始数据中的样本量不同、或如何计算标准误、或如何处理县-年维度份中缺失的薯条销量等等,因为是交互式地进行分析,对这些问题都无法提供百分百确定的答案。


2.效率。如果决定跑一个不一样的回归,如用薯条销量的水平值而非对数形式,必须回到最开头并重复所有整理清洗数据的步骤。当然可以通过保存跑回归之前整理好的数据来避免这个行为,但如果想之后改变对保留和剔除观测值的范围,还是需要退回起点。


在实际中,从原始数据到最终数据可能需要一千步操作。每一步都会面临多个替代方案,走多种弯路,尝试和放弃多个实验。随着研究的开发与完善,每一步通常都可能会运行上百次。如果通过互动式地运行和重新运行这些步骤,几乎是不可能的。


因此,大部分研究者都学会了将关键步骤写入脚本中,特别是数据处理和分析的过程。当我们切换到写 .do 文件,拓展分析,并在LaTeX中进行文字处理后,我们在上文提到论文的工作目录则如下所示:



相比最初“充满互动”的方法,毋庸置疑这已经是进步很大了。如果观察这些文件一段时间,我们也许或多或少能够知道它们的用途。


Extract0B.xls 是原始数据文件,chips.csv 和 tv.csv 是从 Excel 导出的 text 文件,tvdata.dta 是合并后的 Stata 格式数据文件。


Mergefiles.do 和 cleandata.do 是用于整理数据文件的脚本,figures.do、regressions.do 和 regressions_alt.do 是用于数据分析的脚本,.log 和 .eps 是输出文件。


Tv_potato.tex 是论文写作文件,tables.txt 包含了表格,tv_potato_submission.pdf 是我们最后提交给期刊的 PDF 格式文件。


但如果真的要着手复现 tv_potato_submission.pdf 文件,我们会遇到一系列的问题:


◆ 应该将 Extract0B.xls 中的所有观测值导出来吗,还是只需导出没有缺失值的观测?

◆ 先运行 cleandata.do,还是先运行 mergefiles.do?

◆ Figures.do 和 regressions.do 的运行先后顺序有影响吗?

◆ Regressions_alt.do 的输出结果真的在论文中使用了吗,还是只是额外测试了一下?

◆ Tables.txt 是什么文件?这是手动生成的还是代码生成的?

◆ 在 log 文件中的哪个数字是需要在论文中报告的?

◆ Tv_potato_submission.pdf 仅仅是 tv_potato.tex 的 PDF 版本吗,还是我们在投稿期刊之前对格式做了额外的处理?


我们猜测许多读者对还原以上目录的过程深有感触,这和他们希望对 RA 或者合作者的工作目录、或者甚至是几个月前他们自己的工作目录进行还原,是类似的。


在这个小例子中,假设我们不做类似于生成了 PDF 文件后,还在修改或者重新运行 regressions.do 的傻事,只要我们愿意花时间,就能够再复现出论文。但大部分人都知道这是非常痛苦的,如果是一个相对复杂的项目,很有可能还原整个流程需要度过令人沮丧的数天或者数周,而且一些非常“蠢”的错误会让复现几乎成为不可能。


为了让我们的工作目录更具可复制性,我们需要将更多步骤自动执行。我们需要一个方法来储存以上步骤运行的顺序。


首先,新建一个名为 export_to_csv.stc 的 Stat/Transfer 脚本,以解决从 Excel 中转换的问题。(也可以在 Stata 中直接使用 import excel 命令实现。)接着,将原来输出的 tables.txt 文件转为直接在 Stata 中使用 outreg 命令输出名为 tables.tex 的LaTeX文件。


最后,在目录中创建另一个关键脚本,命名为 rundirectory.bat ,这是一个 Windows shell 脚本。其内容如下:


---- rundirectory.bat ----

stattransfer export_to_csv.stcstatase -b mergefiles.dostatase -b cleandata.dostatase -b regressions.dostatase -b figures.dopdflatex tv_potato.tex


这个 rundirectory.bat 脚本就类似于一个路线图,告诉系统应该如何运行目录中的文件。重要的是,这个脚本也告诉了读者这个目录是如何运行的。但与 readme 文件对每一步分析都进行注释不同,rundirectory.bat 不能是不完整的,不能是不清晰的,其语法也不能过时。


在实际操作中,现在我们可以删除目录中所有的输出文件,比如 .csv, .log, .eps, .tables.tex, .pdf 等等,重新运行 rundirectory.bat 后,我们能再次输出这些文件。准确而言,至此,我们的结果是可复制的了。


写一个类似于 rundirectory.bat 的 shell 脚本是容易的。1同时一些调整也是必要的,比如将 Stata 加入系统路径中,无论如何许多类似这样的工作都会非常有用。你也可以将这些步骤写入 Stata 脚本中 (rundirectory.do) ,但系统 shell 文件会让在内部调用不同软件、运行移动或重命名文件等操作更加自然。


当然, rundirectory.bat 不能让所有步骤都实现自动化。我们确实可以(当然,只是试着)写一个 Python 脚本来让论文投稿给期刊,但那看起来对我们来说也是难以完成的。


另一方面,我们持续地发现每次突破边界让操作更加自动化时,都为我们带来了极大的好处。投入的成本往往会比它们看起来的少,而收益是更高的。在研究中的一个准则是,每一个步骤最后需要运行的次数,往往比你预想的多。并且,重复手动处理的成本会迅速累积,最后超过你一次性投资在实现可重复操作上的成本。


我们以前会习惯将 Excel 文件手动导出为 CSV 文件。直到我们在一个项目中,要从一个 Excel 表格中导出 200 个不同的 txt 文件,我们才发现以前的方式已经不适用了。最初,我们还是依据习惯去手动导出,一段时间后,数据提供商给了我们一个更新了数据的新的 Excel 文件。我们就从中吸取教训了。



Part 2

版本控制 (Version Control)


规则

(A) 用版本控制 (Version Control) 来储存代码和数据

(B) 在放回库 (chect it back in) 前先运行整个目录


在上一章,我们展现了电视和薯条项目的文件目录,当我们着手了一阶段后,其中的关键文件如下所示:



日期将文件版本进行划分。字母姓氏缩写( JMS 指代 Jesse,MG 指代 Matt )用于标明作者身份。


将同样的文件以多个版本储存是有很多好处的。最明显的好处是如果有一个步骤是不正确的,这可以让你迅速回溯到以前的版本。另外是让对比变动内容更加容易了。


比如 ,Matt 想要给 Jesse 展示他认为主回归应该怎么改,那么创建 regressions_022713_mg.do 文件就是一个很好的告诉 Jesse 他的想法的途径。即使 Jesse 不认可,也可以随时删掉这个文件。


这个目标是值得称赞的,使用的方法却是错误的。原因有二。


其一,该方式徒增痛苦。研究者需要决定什么时候去创建新版本,什么时候继续编辑旧的(因此会有 _022113a)。研究者也需要在每个文件中标注作者和日期。如果不这么做,很可能会疑惑,为什么文件名显示是2月21日,但系统显示最近的修改日期是在三月份?


另一个疑惑是,而且可能更为重要,我们要理解为什么这种“日期和姓名缩写”的方式是低效的呢?看看上面的文件名字并回答以下的问题:


◆ 哪一个 log 文件是 regressions_022713_mg.do 输出的呢?是不是这个作者(此处要批评 Matt!)没有在代码中改掉输出文件的名字,导致 regressions_022413.log 被重写了?或者他只是简单地没有输出 log 文件?


◆ 哪个版本的 cleandata.do 输出了 regressions_022413.do 用到的数据文件?是否标签为 022113a 的文件是 2 月 24 日前最后更新的文件?又是否 regressions_022413 文件在 2 月 24 日创立但后期又更改了,所以有可能需要 cleandata_022613.do 的输出结果才能正确运行?


遗憾的是,我们没有对 tvdata.dta 标注日期和姓名缩写,可能因为如果要标注的话,每次运行都要在每个新版本文件中的三个地方改文件名,想想这件事情都很恼人...


再观察这些系统日期和文件内容一会,你还是大概率能发现哪些脚本使用到了哪些输入文件的。


那么,如果你吸取了这些教训,之后你会滔滔不绝地提醒你的合作者和 RA 们,要在每一个脚本、LOG 文件和中间生成的数据文件中标注上日期和名字缩写,这样才有可能不会搞混。


但仅仅是保持这么多版本的文件也是很繁琐的。并且一个很严重的风险是,此后你可能挑选不出哪个文件和哪个是一起的,最后你还是不能复制出你的结果。幸运的是,你的电脑已经为你考虑好了,你可以用几分钟的时间去安装一个免费软件。


在我们告诉你真相之前,先说个事实。(毕竟,这是一本面向实证研究者的指南。)你的电脑、手机、平板电脑、汽车或任何其他现代计算设备上的商业软件,没有一个用了这种“日期加人名缩写”的命名方法。


相反,软件工程师用一个叫版本控制器 (version control) 的软件来追踪代码的历史版本更迭。版本控制的运行方式如下。在电脑(或者更好的是在远程服务器中)建立一个“ 版本库 (repository)”。每次你想去修改目录,都可以在库中取出来(“check out”)。在你修改完毕后,再放回版本库中(“check in”)。这就完成了。你不需要修改文件名,不需要增加日期,或者进行任何其他改动。因为这个软件将所有放回版本库的版本文件全部都记录下来了。


如果你改变了你的想法,可以怎么做呢?你可以在软件中查询到目录的历史变动,且如果你想变回到原有版本的目录,或者仅仅是一个文件的原有版本,只用点击一下鼠标就够了。


此外,如果你的合作者偷偷改变了回归的主模型设定但没有告诉你,会发生什么呢?版本控制软件会自动记录每个变动是谁改的。如果你想看改动都在哪里,一些先进的程序包将通过颜色对比来展示,哪行代码被修改了和改了什么。2


这个方法为什么好?为什么软件工程师必须使用这种工具?因为它在每时每刻都为目录保留了一个简单而权威的版本。在极少数情况下,两个人会想要给同一个文件同时进行冲突的修改操作,这个软件将发出警告并帮助他们解决这些冲突。


另一个重要的附属好处是你想要修改的时候不必畏畏缩缩了。如果犯了一个错误,或者你开启了一个新方向又后悔了,你总是可以轻松地回到原始状态,或者只进行了部分修改的状态。这样做并没有要求保留日期和人名缩写。所有的文件名字都可以和最开始设想的一样。软件为你解决了版本的问题,你只需要关注于写代码并让它正常运行。毕竟,你花了六年的时间读研究生,不是为了到处去输入今天的日期滴。


使用了版本控制之后,你的生活会得到多大的改善呢?想象一下在“撤销”命令发明之前,文字处理会是什么样子。一次错误的敲击可能就意味着灾难。版本控制就像为任何事物设立了撤销的命令。


总之,在本章我们的第一条准则就是,所有的操作,无论是代码还是数据,都要在版本控制下进行。


实际上,版本控制对论文写作草稿等文件也是大有用途的。它让你能够无忧无虑地进行任何改动,也能追溯到改动人等等。(一些读者也许会发现 “GoogleDocs” 的 “历史版本” 特性非常好用,这恰恰是以软件工程的版本控制模型为基础的。使用了版本控制后,你也可以去真切体会LaTeX、LyX 、或其他任何你喜欢的写作程序包有多么好用。)


但如果你想最大化地使用这个方法,第二个规则是你必须在将变动放回库 (check in) 之前运行整个目录。回到我们之前的例子,现在我们丢掉恼人的日期和人名缩写标记,并将 rundirectory.bat (就是将每个脚本从头到尾运行一遍的文件)放回去。



假设 Jesse 修改了 cleandata.do 并在运行后生成了新的 tvdata.dta 。如果 Jesse 放回了那个改动,Matt 可能随后会发现 regressions.do 无法正常运行了,因为 regressions.do 没有预期到 tvdata.dta 中的改动。


如果要解决这个问题,并保证之后不会再次发生的话,只用从头到尾运行一次 rundirextory.bat ,在 check in 当前目录之前检查出错误就可以啦。如果你每次 check in 的版本都已经通过 rundirectory.bat 运行成功,那除非软件自己变了,每次 check out 这个版本,你都会得到与原有 regressions.log 一样的输出结果。


请注意上述问题不是使用版本控制软件才会有的问题。


“日期加人名缩写”的方法也有可能存在这种目录内的冲突 (within-directory conflict) ,究其原因是我们如果要弄清楚哪个脚本用到哪个文件,需要的精力实在太多了。


相反,使用版本控制,再加上完整运行目录的check in 规则,就在保证可复制性和撤销重来的同时,提供了最省力的综合解决方案。


好的,现在你应该被我们说服了。接下来做什么?手把手教你建立和使用版本控制软件已经超出我们的范围了。


但是值得关注的是,我们在 Windows 中使用的是 SVN 中心版本库 (SubVersion repository),同时用 TortoiseSVN 这个非常好看的 Windows 客户端来进行交互 (interact)。Mac 系统也有类似的软件。近年类似于 Git 或者 BitBuket 的版本控制方法也值得学习。建立一个版本库并学习如何使用很可能要花费一些日子,但在其中投入一个月或者两个月的时间也不亏的。



Part 3目录 (Directories)


规则

(A) 按照功能将目录分类

(B) 将文件按照输入与输出类型分类

(C) 让目录更加简洁轻便


让我们再回到我们土豆薯条项目的主目录:


以上目录包含了整个项目的所有步骤,由单个批处理文件 (batch file) rundirectory.bat 进行控制。


这样一个包含并完成了所有事项的目录看起来是有些吸引人的,但对于大部分现实中的研究项目,这种组织方式并不是理想的。


考虑以下的情景:


1. 如果想要改变一个回归的设定,但是不想把所有的数据产生过程都重新做一遍。

2. 研究者学习了简洁的 Stata 代码,写了一个 export_to_csv.stc 脚本但其在 tvdata.dta 之外输出了不必要的 tv.csv 和 chips.csv 文件。


在进一步展开之前,我们需要提醒的是,这样做会让研究者必须完整浏览 regressions.do 和 regressions_alt.do 文件,才能够确保这些脚本除了 tvdata.dta 之外,是没有用到 tv.csv 和 chips.csv 的。


如果我们考虑以下目录结构构成的替代方案(为了简便,没有纳入TEX和 PDF 文件):



总体共有两个高阶文件夹。一个包含的代码实现了从原始输入到生成供 Stata 方便运行的文件的过程 (build 文件夹),另一个涵盖的代码则将 Stata 文件转化为论文中所需的图形和表格 (analysis 文件夹)。


在每一个高阶目录之下都有一个相同的子目录结构,以区分出输入、输出、代码以及临时或中间文件。每个目录都被一个可以从头到尾运行一遍的名为 rundirectory.bat 的单一脚本所控制。(实际上,我们提倡在 rundirectory.bat 的代码开头,先去清除所有在/temp和/output文件夹中的内容,这样才能确保所有输出文件都是用当前的代码得到的。)


现在就能只修改分析部分 (analysis) ,而不用重新运行建立数据的过程 (build) 了。并且现有目录结构也很清晰,在分析部分的代码中,只用到了 tvdata.dta,而 chips.csv 和 tv.csv 是明确作为临时文件而已。


最后,因为我们在本地对输入数据进行了链接,我们就可以在 analysis 目录下所有的代码中使用本地引用 (local references) 了,即 ../input/tvdata.dta 而非 C:/build/output/tvdata.dta 。3


这种结构也存在缺陷,如在另一个设备上运行(此时 tvdata.dta 文件的引用链接将不再有效)、或者 tvdata.dta 的结构发生改变, C:/analysis 目录下的代码都会发生故障。


所以我们在实际中的操作方式会是稍微复杂一些:相比于使用 tvdata.dta,我们链接的是放在云共享储存空间的固定版本 (fixed revisions) 数据集。这意味着在有网络的情况下, /analysis/ 目录下的文件可以在任何地方顺利运行。


这个固定版本一定程度意味着如果有人修改了数据的结果,并将这个新版本放回库中,分析模块的代码也能够继续工作,因为它链接的还是之前旧的那个。(当然有时候可能有人希望在代码中替换指向到新的版本,但用户能够自己决定什么时候做这件事情,而不是在运行代码时意外被打断。)


我们只是概括了一些使用依据功能性目录来组织代码的好处。当然还有许多其他好处。例如,在任何目录下都可以轻易获取到 C:/build 中的输出文件,这使得你有许多项目使用到同一个文件时,不再需要重复创建大量多余的复制文件。另外,将脚本依据功能放入到不同组之下,会让 debug 更加简单迅速。



Part 4键 (Keys)


规则

(A) 将清洗后的数据保存在唯一的没有缺失值的关键码(Key,简称键)中

(B) 在代码流水线 (code pipeline) 中,尽可能保持数据规范化 (normalized) 直到最后一步


众所周知,电视首先会选择进入大城市。因此,在分析电视对薯片消费的影响时,要求质量较高的人口数据作为控制变量。为了方便分析,我们请了一位 RA 搜集人口数据集。这个数据集具体如下:



这也太混乱了,漏洞百出!纽约州 (NY state) 一个县 (county) 的人口 (state_pop) 数据是 4300 万但是另一个 county 的人口数据缺失,怎么可能呢?如果这是一个以县为观测单位的数据集,“县 (county)”变量有空值,说明什么呢?如果 region 变量是类似于人口普查区域 (Census region) 的定义,同一个州的两个县怎么会在不同的 region 中?4并且,纽约州的所有县都以 36 开头,为什么会有一个县的 state 变量缺失呢?


我们不能使用这个数据集了,我们不知道它们都代表什么。在回去查看底层代码之前,我们不能说出每个变量是什么,甚至不知道每一行是什么。那我们也不用去尝试将其与另一个数据集合并到一起了。我们又如何知道 county 变量值为 36040 对应的 state 是哪个?或者哪个 region 对应了 county 值为 37003 的观测呢?


根据我们的了解,许多研究者都会花时间与类似这样的数据集斗争,并态度强烈地让 RA 、学生或者合作者去解决这些问题。


其实一定会存在一个更好的方式的,因为我们知道像金融机构、零售商和保险公司等大型机构,都必须与更加复杂的数据打交道,而且这些错误会造成很严重的后果。


很早以前,机智的人们发觉了一个数据库设计 (database design) 的基本原则:数据库的物理结构 (physical structure) 必须与其逻辑结构 (logical structural) 互通。


如果你把县级人口普查数据给一个培训过数据库的人,你可能会得到如下的反馈,其命名为关系数据库 (relational database) :



现在我们的疑问就消失了。每个县都对应了一个人口变量和一个州变量。每个州也都有一个人口变量和一个区域变量。没有州、县变量数据缺失,也没有冲突的定义。数据库是自文档化的 (self-documenting)5。实际上,现在数据库已经非常清晰了,以致于我们可以不去使用 county_pop 或 state_pop 的命名,只用 population 就可以了。所有人都能明白你指的是哪个主体的人口数据。


注意在这里提到的关系数据库,我们只是用于说明数据库是怎么构成的,而不是说使用其他花俏的软件。上述的数据可以储存为以两个制表符为分割的文本文件,或者两个 Stata .dta 文件,或者两个任何支持矩形数据 (rectangular data) 的标准统计软件包的文件。


再回看这个例子,一些关键准则在这里发挥了作用。我们现在需要引入一些专业词以便更好理解。以矩形数组 (rectangular array) 储存的数据命名为表 (table)。上述例子就有一个县表 (county table) 和一个州表 (state table)。我们将表中的行称为元素(element),列称为变量 (variable)。


重要的是,每个表格都有一个关键码(Key,简称键) - 规则 A。键是用于唯一识别表中元素的一个变量或者变量集。变量构成的键从不会有缺失值,且它的值也不会在表的不同行中重复出现。因此,州表中有且只有一个值为 New York 的行,且没有 state 变量缺失的行。


表的每个变量都是表中元素的一种属性 (attribute)。县人口 (county population) 是县的属性,所以存在于县表中。州人口 (state population) 是州的属性,所以不会出现于县表中。


如果我们拥有县级层面的面板数据,我们需要对在县层面的变动(如州变量)和在县-年层面的变动(如人口变量)分别有两张不同的表。


这也是大部分研究者用于分析的数据文件没有满足数据库的准则的主要原因:他们一般都会把在不同层面变动的变量合并在一个总的文件中。


表中的变量可以包括一个或者多个外键 (foreign key)。外键就是数据库中另一个表的键。在县表中,“state” 变量就是一个外键:它将县与州表的元素配对联系在一起。外键与其他变量遵循一致的规则,他们都具有逻辑上的层级性。州在区域 (region) 中,县在州中,所以 “state” 出现在县表,而 “region” 出现在州表。


以上述方式储存的数据被认为是规范化的 (normalized)。储存规范化数据意味着你的数据将更容易理解并且将更难犯后果严重的错误。

大部分数据分析软件都不会在关系数据库上直接跑回归。在分析之前,需要进行将表合并(merge,或者在数据库范式中称为连接 join),以产生一个单独的矩阵数组。另外,我们需要计算一些原始数据中没有的变量,比如人口数量的对数等。


从下载、输入或直接从原始源购买数据,到得到你用于估计的矩阵,我们推荐以下三个步骤。


第一,将原始数据储存在规范化文件中,保留原始源数据的信息并遵守上述的规则。不要考虑你计划如何使用数据。相反,应当想象你是准备向有着不同需求的更大用户群体发布数据。这么做因为很可能无法预料到你未来会如何使用这个数据。


第二,建立第二个规范化文件的集合,需要覆盖在未来分析中使用到的由原始变量转化生成的变量。比如,你可能会在县表中加入一个反映该县人口在其所属州中排名的变量。当然这个阶段,你也可以从其他数据库中导入变量,如利用地理数据库来获得经纬度等。


第三,将数据库中的表合并为用于模型估计的矩阵数组。眼下你的数据库还是应当有唯一的没有缺失值的键,但它不一定要是规范化的。在我们的例子中,你可以在县层面的文件中包含并不是县属性的变量,比如区域 (region) 变量。如果是面板数据,你的文件需要同时包含 县层面 和 县-年份层面 的变量。不要在这个步骤中进行数据处理。如果你的分析要求了州人口数据的对数,在数据库符合规则的条件下进行计算。


遵守上述的步骤意味着直到代码的最后一步,你就能保证你的数据是处于规范化格式之下的 - 规则 B。



Part 5抽象 (Abstraction)


规则

(A) 利用抽象 (Abstraction) 减少冗余

(B) 利用抽象使更加清晰明了

(C) 否则,不要使用抽象


进一步探索薯片消费的空间关系。我们想测试县人均薯条消费是否与同一州其它县的人均薯条消费相关。首先,我们要用每个县的人均消费来定义 “leave-out” 平均:


egen total_pc_potato = total(pc_potato), by(state)

egen total_obs = count(pc_potato), by(state)gen leaveout_state_pc_potato = (total_pc_potato - pc_potato) / (total_obs - 1)


现在我们可以来检验 pc_potato 是否和 leaveout_state_pc_potato 相关。如果相关,则我们可能需要去调整在模型中使用的标准误。检验后,我们发现空间上的关系并不存在。


但如果我们错误地使用了总体水平去衡量呢?也许空间关系会在都市区域 (metropolitan area) 层面出现呢?为了探究这个话题,我们复制粘贴了上述代码,并依据都市区域而非整体层面进行了修改,代码如下:


egen total_pc_potato = total(pc_potato), by(metroarea)

egen total_obs = count(pc_potato), by(state)gen leaveout_metro_pc_potato = (total_pc_potato - pc_potato) / (total_obs - 1)


说到这里,也可以检测一下是否在平均家庭消费而非人均消费上会存在空间关系。基于此,我们可以定义第三个 leave-out 平均:


egen total_hh_potato = total(hh_potato), by(metroarea)

egen total_obs = count(hh_potato), by(state)gen leaveout_metro_hh_potato = (total_hh_potato - pc_potato) / (total_obs - 1)


聪明的你不知道是否已经发现了错误。


在第一个“复制粘贴”过程中,我们没有在其中一行代码将 state 替换为 metroarea。


在第二个“复制粘贴”中,我们不仅重复了第一个错误,且没有将人均薯条消费的变量替换为家庭层面的类似消费变量。以上的代码确实可以顺利运行,但在第一个代码块之后,所有结果都将是错误的。


考虑以上复制粘贴方法的替代方案的话,我们可以写一个一般化函数计算出变量的 leave-out 平均:


program leaveout_mean

    syntax, invar(varname) outvar(name) byvar(varname)    tempvar tot_invar count_invar    egen `tot_invar'= total(`invar'), by(`byvar')    egen `count_invar'= count(`invar'), by(`byvar')    gen `outvar' = (`tot_invar' - `invar') / (`count_invar' - 1)End


定义了以上函数,我们可以将上方三个代码块替换为下面的三行:


leaveout_mean, invar(pc_potato) outvar(leaveout_state_pc_potato) byvar(state)

leaveout_mean, invar(pc_potato) outvar(leaveout_metro_pc_potato) byvar(metro)leaveout_mean, invar(hh_potato) outvar(leaveout_metro_hh_potato) byvar(metro)


现在需要复制粘贴的数量就被最小化了:每次输入我们都只是逐行变动了每行的一个地方。并且,leaveout_mean 函数是完全一般化的,我们也可以使用在其他项目上。6再也不用去从零开始编写代码计算 leave-out 平均了。


达到上述目的的关键是需要意识到,上述三个代码块都是同一个抽象理念的具体运用:除去给定的某个观测,对于同一个组里的所有观测,计算某个变量的平均值。在程序设计中,将对于某个事物的具体运用转变为一般化目的的工具,就是抽象 (abstraction)。


抽象对于写出高质量的代码是十分重要的,主要有两大原因。第一,和我们上述领悟到的一样,它将减少冗余,缩减了犯错的空间,提高了你在写代码中获得的价值。第二点也很重要,它将代码变得更具有可读性。7


对于读者而言,看到上面三个代码块中的任意一个,可能很容易不知道它们的目的是什么。相反,一个名为 leaveout_mean 的程序则是容易理解的。


但是抽象也不能被滥用。如果一个操作只会被执行一次,并且用来运行的代码也很容易阅读,则就不建议使用抽象了。没有目的地运用抽象,会让你花很多的时间去处理未来永远不会遇到的状况上。


当你计划会频繁使用到一个函数,你应该花费时间在仔细地执行上。我们发现软件工程实践中用到的“单元测试 (unit testing) ”是很有帮助的8


这意味着需要写一个脚本,彻底测试其间函数的具体运行,确保达到了预期的效果。举个例子,我们生成虚拟数据,并检验 leaveout_mean 函数是否正确算出了 leave-out 平均值。


单元测试的一个优势是它让你能够安全地更改函数,而不必担心引入错误而破坏了代码。它也提供了一个记录函数如何运行的简便方式:要求输入什么,输入什么的时候会报错等等。


抽象并不仅仅局限于代码。任何时候你发现在重复进行一个操作了,和抽象都是相关的。比如本章提到的原则,解释了为什么文字处理包会附带类似备忘录或报告类型等常用的标准文档。并且这些原则也解释了为什么我们要写这个 handbook,而不是重复地告诉我们的 RA 我们为什么这样写代码!



Part 6文档编写 (Documentation)


规则

(A) 不要撰写不会维护更新的文档

(B) 代码应当是自文档化的 (self-documenting)


我们已经估计了电视对薯片消费的影响。为了阐述其对社会的有害影响,我们希望进行福利分析,其中需要计算弹性。幸运的是,Jesse 的毕业论文研究的是某税率上升时对马铃薯需求的影响,因而从中可以直接得到需求弹性的数值。


以下是 Stata 代码的一部分:


* Elasticity = Percent Change in Quantity / Percent Change in Price

* Elasticity = 0.4 / 0.2 = 2* See Shapiro (2005), The Economics of Potato Chips,* Harvard University Mimeo, Table 2A.compute_welfare_loss, elasticity(2)


请注意这些有用的注释将为读者提供一个方向。


我们知道的许多研究者在说服他们自己、他们的合作者和他们的助研去更多写类似上述注释上花了很多的力气,一般而言,就是要在脚本和数据文件自身之外,去细致地记录他们的代码和数据是如何组织的。你可能预期我们也会倡导这样。毕竟,我们执着于对事物进行组织管理。但在本章节,我们尝试去说服你要更少记录,而不是更多。为什么呢?请听我们继续道来。


在我们完成了第一个版本脚本的几个月后,我们返回去对我们的代码进行修改,如下9


* Elasticity = Percent Change in Quantity / Percent Change in Price

* Elasticity = 0.4 / 0.2 = 2* See Shapiro (2005), The Economics of Potato Chips,* Harvard University Mimeo, Table 2A.compute_welfare_loss, elasticity(3)


注意红色加下划线部分是互相冲突的。


也许一些人会认为这是在原始计算中存在的印刷错误,或者使用的是 Jesse 毕业论文中 Table 2B 中的估计值而非 Table 2A 的数值。无论错误起源于哪里,问题都很明显了:代码中的注释与代码本身冲突,并且不知道哪个是正确的。一些人一定会返回到源头去搞清楚到底应该使用哪个数字。


读者们也可能会发现这其实是普遍面临问题的一个例子:任何时候相同的信息都会有不止一种衡量(在这个案例中,比如弹性),你就会面临某天两者冲突的风险。最好的情况下,你只需要花费时间去解决这个问题;最坏的情况下,你会得到错误的结果或者内部发生矛盾。


内部矛盾的问题如果是涉及到诸如注释、笔记或自述文件 (readme) 等文档,就会特别严重了。因为你不必须更新这些文档,你的代码也会正常运行,也会得到正确的结果。因此,尝试改进代码,而没有同步更新注释,到头来这些注释只会让你徒增烦恼。上述例子就说明了,注释的陈旧可能会比一开始就不写这类文档带来更多的麻烦。


为了避免这种情况发生,你需要不断更新你的注释,和你的代码保持同步的更新。如果不值得以这样的标准去维护一个文档,那一开始就不值得去编写这个文档了,这也是我们前文所说的规则A。


现在一个重要的问题就出现了,怎么样才能不使用大量的注释,而让代码清晰可读呢?如果本章上文提到的代码不加入任何注释,读者怎么知道为什么弹性是 2 而不是 3 呢?


为了解决这个问题,我们只能从代码本身入手了。很多注释中的信息都可以轻而易举地并入代码中:


* See Shapiro (2005), The Economics of Potato Chips,

* Harvard University Mimeo, Table 2A.

local percent_change_in_quantity = -0.4

local percent_change_in_price = 0.2

local elasticity = `percent_change_in_quantity'/`percent_change_in_price'

compute_welfare_loss, elasticity(`elasticity')


上述代码块包含的文字数量上和我们一开始编写的文档一样多,其实只是清晰地说明了我们计算价格弹性和数量构成的表达式。但这样就好多了,大大减少了内部发生冲突的概率。当你改变数量变化百分比的时候,不可能不改变弹性的数值大小;如果不改变这些百分比变化数值,你也不会得到一个不同的弹性数值。


如果可以,你写的代码应当是自文档化的 (self-documenting) 5,也就是规则 B 。利用变量的命名和代码的结构,将更好地引导读者去运行和理解你的代码。总之这是一个好的方法,因为即使是最好的注释也无法解开代码中的谜团。再者,这意味着你不必要去写注释或其他的笔记了,不然你到头来还是会发现不知道代码在做什么。


这些原则远比代码重要,并且它们构成了本手册中其他章节的基础。组织好数据文件,通过结构来清晰表达它们的含义,你就不用编写大量文档说明每个数据集如何配对(第五章)。对文件、目录和其它对象巧妙地命名,使它们的名字呈现它们的功能(第四章)。一张绘制巧妙的图形或表格往往能说明很多问题,以至于笔记只是为了说明显而易见的/澄清次要的细节。诸如此类。


编写文档确实有一定的作用。在上述例子中,如果我们不引用 Jesse 的论文,读者怎么知道 0.4 哪里来的呢?而又没有可行的在代码中放入能够直达原始论文网址的方式,这时使用注释是更合适的。


编写文档可以用来说明实际上正确的事物,也许它第一眼看起来是错的。假设,我们有一个变量 服从对数分布,位置参数为 ,形状参数为 。如果我们想要计算该变量期望的对数,我们最好这样写代码:


* Log of the expectation is not the expectation of the log

* See http://en.wikipedia.org/wiki/Log-normal_distributionlog_expected_y = `mu' + 0.5*(`sigma'^2)


这样读者将不会奇怪为什么其表达不是简单的log(E(y))=U 。当然,文档扮演的角色取决于读它的人:如果你和你的合作者都不可能忘记对数分布平均值的表达,那上述的注释大概率是多余的。


另外,编写文档也可以用来阻止无意识的行为。假设你写了一个用最大似然法估计回归的命令。如果存在两个或者更多的变量是共线的,你的求解程序将无限迭代。因此,你希望在代码中放入一个提示性的警告:“不要尝试去估计一个无法估计的模型。”但是要注意,相比文字的记录,代码本身是更好的。写一个测试矩阵 (X’X) 是否满秩的函数,可以提供和文字相同的信息,但不会要求读者要足够细心去读注释,并且能让问题得到更快的解决。


这也让我们想起了一件事。在 Jesse 的家里,有一个带两个开关的炉室。一个控制灯光,另一个则控制整个家里的热水。某人在第一次进入他家时(让我们不要说出这个人的名字),在试图寻找控制灯的开关的过程中,不小心关掉了热水。Jesse 曾经贴过一个“不要碰这个开关”的标签。但在黑暗中的焦急摸索下,这个标签显然是无价值的。所以他直接在这个开关上贴了一段胶带。当你真的真的想阻止某些输入的时候,类似于“不要做 XXX ”的注释是没有的。写一段直接阻止这些输入的代码,将从一开始就将其拒之门外。



Part 7管理 (Management)


规则

(A) 通过任务管理系统 (task management system) 来管理任务

(B) 电子邮件不是一个任务管理系统


请看下面的邮件:


From: Jesse Shapiro

To: Matthew GentzkowRe: 薯片你好 Matt,我们控制了每个县的乡村色拉酱销售量之后,你有进行稳健性检验吗?我正在写蘸酱的部分,同时想要提及这一事项。-Jesse




From: Matthew Gentzkow

To: Jesse Shapiro对不起,我以为你做了,因为这和另一个你正在进行的控制调味汁销量的事情是类似的。请告诉我,是你来做这件事情还是希望我来做。-MG

From: Jesse Shapiro

To: Matthew Gentzkow, Michael Sinkinson我以为 Matt 正在做乡村色拉酱的研究,而 Mike 在做调味汁的?-Jesse

From: Michael Sinkinson

To: Matthew Gentzkow, Jesse Shapiro我在两周前做了调味汁的稳健性检验,请见我在 8 月 14 日上午 9:36 发送的邮件。-Mike

From: Jesse Shapiro

To: Michael Sinkinson, Matthew Gentzkow是的,但在那个邮件里面,你控制了调味汁消费的对数。我以为我们达成了一致,希望控制消费的绝对水平。-Jesse

From: Michael Sinkinson

To: Jesse Shapiro, Matthew Gentzkow好的,我马上去做!-Mike


以上的邮件存在什么问题?主要是,过于模糊了。Mike 认为他的任务已经完成了,而 Jesse 认为没有。Matt 认为 Jesse 在解决乡村色拉酱的话题,而 Jeese 认为 Matt 在着手做这件事情。实际上,细心的读者会发现以上所有的邮件,还是没有说清楚谁要去做乡村色拉酱的稳健性检验!


更糟糕的还有。如果我们返回到两周前的调味汁课题,我们要怎么知道它进行到哪一步了呢?看以上的邮件?还是 Mike 提到的 8 月 14 日的邮件?以及提到的讨论哪里有记录呢?通过日期来看吗?再看这一整系列,我们能知道所有关于乡村色拉酱的其他讨论信息吗?


如果你是独立工作,这些问题都是小问题。你大概可以用记事本,Word 文档或者白板来追踪记录你需要做的工作。当然有时候你可能会忘记你计划了什么,或者你在哪里记下了什么;但如果你是一个有条理的人,应该还是没有问题的。

但如果是两个人合作,以上反映的问题将被放大。虽然我们没有严肃地去证明,但我们认为问题的严重性将随项目参与人数呈现指数型增长,比如合作者、RA 的加入等等。


软件公司会系统地去处理项目和任务的管理。比如微软公司就不会仅仅去说:“你好 Matt,如果你有时间,可以在 Word 中添加行间的拼写检查功能吗?”


准确地说,企业在团队协同工作时需要使用到任务和项目管理系统,强迫了针对任务和项目进行有组织性的交流和汇报。以往的时候,这些工作通常是递交实物报告给指挥系统,现在则越来越多地使用基于浏览器的任务管理门户网站。


在其中的一个网站,Mike 的调味汁任务可能是如下的:


任务:调味汁稳健性检验

指定处理人:Michael Sinkinson任务分配人:Jesse Shapiro评论订阅人:Matthew Gentzkow状态:已完成。说明:在主回归中加入人均调味汁消费量的控制变量。在稳健性检验表格中增加一列来说明结果。

评论人:Michael Sinkinson

正在进行!

评论人:Michael Sinkinson

详见 /drafts/Potato Chips 中发布的新版论文,和 /analysis/Potato Chips 中对应的代码。这是你想要的结果吗?

评论人:Jesse Shapiro

大体上是的。我们的计量模型表明,调味汁的消费应该以绝对水平形式出现,而不是对数形式。你可以修改一下吗?

评论人:Michael Sinkinson

OK了,现在可以吗?

评论人:Jesse Shapiro

好的,现在看起来不错。

完成人:Michael Sinkinson


至此,关于这个任务谁来负责,其目的是什么,已经很清晰了。任何人看到这个任务的开头就会知道 Mike 是需要做这件事情的人,也不需要通过什么沟通来确认谁在做什么。


同时,这也很自然地为针对这个任务进行交流保留了空间。每个人都预期了问题和解答会发布到对应任务的下方。以及,日复一日年复一年,关于特定任务的记录仍然会存在,谁做了什么以及为什么这样做。


现在也有很多类似于上面的在线任务管理系统。许多是免费的,或至少提供了可以免费满足最基本服务的选项。大多数也都有对应手机端的应用程序,并提供某种电子邮件的集成,例如,Mike 在上述的评论可以通过电子邮件发送给 Jesse,这样他就知道有事情需要他核查了。


这些系统在不断变化更新,以及你喜欢哪个往往是和品味、风格、预算等等相关的,所以我们不会把它们全部列出来。一些好的免费的选择有:Asana (www.asana.com),Wrike (www.wrike.com) 和 Flow (www.getflow.com)。我们使用一个叫 JIRA 的程序,需要收费,在安装方面也会比较费劲。


刚好我们谈到了有用的工具,你也应该为自己提供可以协同记录笔记的环境。那样,你就不会被任务管理系统规定的能够分享或者记录的内容所限制。有一个能够随意记下想法或者展示结果的地方是很有好处的,也许没有在最终版本论文中用到的代码那么规范,但会比使用电子邮件或者对话保留得更长久。


最好的系统是可以让你轻松地依据项目组织笔记,并与其他人共享。如果可以添加丰富的附件,以便向你的合作者展示图表、代码片段、表格等,那就更好了。


这也有一系列选择,并且大多数也都是免费的。Evernote (www.evernote.com) 提供了一个免费的基础选项,也可以在多个平台和接口中交互使用。对于 Windows 用户来说,Microsoft Office 里的 OneNote 也是一个不错的选择。





转载/开白

扫码添加学术小秘书

备注「开白」



注释

1. 如果你不使用 Windows 系统,Linux 的 shell 文件也能够同样运行。如果你可以熟练使用 Python ,你甚至能更加得心应手,你可以写一个 rundictrectory.py 让你能够在 Windows 和 Linux 系统中同时工作。

2. 对于稍微进阶的用户来说,当然也有成熟的方案,比如尝试明晰的代码修改,你可以进行通过更明确的方式进行暂时性的改动,你可以标出这些改动,让你的合作者在你上飞机前核查一下。

3. 在另一个目录中创建“符号链接 (symbolic link)”也是简单的;更多细节可以查看你的操作系统手册。另一个可行操作是可以在 rundirectory.bat 中增加将 tvdata.dta 文件从 /input 复制到 C:/build/output 的代码。这个方法也可以让你在代码中直接引用本地文件,但需要牺牲因复制 tvdata.dta 造成的储存空间占用。

4. 美国人口普查局将美国分为了四个区域 (region),九个分区 (subregion)。更多信息请见:United States Map Defines New England, Midwest, South (https://www.businessinsider.com/united-states-regions-new-england-midwest-south-2018-4) 

5. Self-documenting 含义是代码自己带有文档及较多注释,或者自己具有可解释性,提升了代码可读性,一定程度可以理解为可读性高、对用户更加友好

6. 在 Stata 中,和你所有可能用到的程序一样,想要随时制作一个可移植和可访问的程序是很容易的。

7. 事实上,我们已经发现一个函数的一般化版本通常更容易编写,也更容易阅读。(要知道为什么,可以对比一下,为特定的变量矩阵进行线性回归编程,和为一个一般化的变量矩阵进行线性回归编程,两者的困难程度差异。) 

8. 百度百科:单元测试是指对软件中的最小可测试单元进行检查和验证。

9. 下方代码因需要改变字体颜色,在写作时使用 html 语法,未使用 Markdown 自带的代码块环境。

我就知道你“在看”

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存