Stata论文复现:做一个优雅的码农

发布时间:2022-04-20 阅读 1342

Stata连享会   主页 || 视频 || 推文 || 知乎 || Bilibili 站

温馨提示: 定期 清理浏览器缓存,可以获得最佳浏览体验。

New! lianxh 命令发布了:
随时搜索推文、Stata 资源。安装:
. ssc install lianxh
详情参见帮助文件 (有惊喜):
. help lianxh
连享会新命令:cnssc, ihelp, rdbalance, gitee, installpkg

课程详情 https://gitee.com/lianxh/Course

课程主页 https://gitee.com/lianxh/Course

⛳ Stata 系列推文:

PDF下载 - 推文合集

作者: 邹恬华 ( 对外经济贸易大学 )
邮箱: zoutianhua2022@163.com

Source: Michael Stepner 团队的 代码风格文档:
Github 仓库地址:https://github.com/michaelstepner/healthinequality-code


目录 [TOC]


前言:本文亮点速览

使用杂乱无章的代码写作习惯就像背上高息的“技术债务”,不仅使得今后的代码维护变得困难而耗时,而且使得他人几乎不可能重复文章的结果。

本文翻译并整理了 Michael Stepner 团队的代码风格文档,并复制了其团队 2016 年发表在 JAMA 的 论文,以飨读者。代码数据下载地址如超链接所示。文中介绍的代码风格主要包括以下要点:

  • 在处理流程上,遵循原始数据--->派生数据--->临时文件或结果的顺序
  • 在具体实现上,文件夹应当大致划分为代码、原始数据、派生数据、临时文件和结果这五个文件夹。
  • 代码文件夹中最好包含项目使用的从 SSC 上下载的 ado 文件
  • 原始数据文件夹中最好包含一个 source.txt 文件以说明数据来源
  • 临时文件夹中的文件最好进行描述性的、唯一的命名
  • 编写的 do 文件应当尽量简短、独立、专用
  • 使用相对路径而非绝对路径
  • 变量名称设置应当尽量描述性、有意义、不易产生歧义
  • 数据集应当具有一组独特 ID
  • 将运行在需要满足的假设条件以代码形式写出,而非仅仅注释
  • 避免重复的代码,可以使用循环语句、program 语句、ado 文件等方式解决代码重复问题
  • 使用 project 命令实现程序的自动化

开始我们的优雅代码之旅吧!

1. 为什么要规范化你的代码?

如何组织一个项目的代码、数据、文档和结果的方法论被称为代码风格

当您独自处理一个短期项目时,不统一代码风格不会有严重的后果;但当许多人在一个项目上长期合作时,团队共同学习一套代码风格将会有效避免可能的混乱。具体而言,在大型合作研究项目中,您可能面临以下问题:

  • 我刚刚接手这个项目,应当从哪里开始运行这些代码?
  • 我想在数据集中新增一个变量,团队是从哪里下载的人口普查数据?
  • 我想更新图 6B,应当运行哪些代码?
  • 我改变了生成图 6B 的代码,这会影响文章的其它结果吗?
  • 数据集新增了一年的样本,我应该怎么做才能确保文章所有的结果都更新了?

下文描述的代码风格有助于确保上述的问题能够顺利解决,从而节约时间减少错误

2. 实例:健康不平等项目代码概述

本文通过介绍健康不平等这一项目实例来阐述原作者研究团队的代码风格。

这一项目的代码由三个“管线” ( 英文原文 pipelines,亦可理解为流水线、流程 ) 组成,包括:

  • 初始数据生成管线
  • 派生数据生成管线
  • 分析管线

这些管线串连起了原始数据到研究结果 ( 表格、图片、数据等 ) 的研究流程。研究流程如以下流程图所示:

原始数据 ( raw data ) ---通过初始数据生成管线 ( mortality_init ) 和派生数据生成管线 ( mortality_datagen ) ---> 获得派生数据 ( derived data ) ---通过分析管线 ( mortality ) ---> 获得临时文件 ( scratch ) 和结果 ( results )

上述三个“管线”在具体运行时通过 project 命令 ( -project- command ) 实现,该命令属于自动化工具 ( build automation tool ) 的一种 ( 将在本文第五部分具体展开描述,译者注 ) 。在存储库的根目录下,包含三个 do 文件:

  • 初始数据生成管线 ( mortality_init.do ) 将作者无权公开的原始死亡率数据处理成可以公开的汇总数据。汇总数据也即本文使用的初始数据。
  • 派生数据生成管线 ( mortality_datagen.do ) 将原始数据处理为派生数据。
  • 分析管线 ( mortality.do ) 将原始数据和派生数据处理成结果。

此外,还有第四个管线单元测试管线 mortality_tests.do 用来验证代码是否按照预期工作。复制文章结果时无需运行这一管线。

3. 文件夹是如何组织的

一个好的文件夹组织方式有助于更好地存放和提取文件。本文提到的健康不平等项目主要包含以下五个文件夹:

  • 代码 ( code )
  • 原始数据 ( data/raw )
  • 派生数据 ( data/derived )
  • 临时文件 ( scratch )
  • 结果 ( results )

以下具体阐述各文件夹包含的内容。

3.1 代码 ( code ) 文件夹

除定义管线的主代码外,所有的代码都应存储在名为代码 ( code ) 的文件夹中。当然,为了便于浏览与查找,代码文件夹包括子文件夹。子文件夹可以根据需要随时自由安排。

代码文件夹中有三个特殊的子文件夹:

  • 自己撰写的 ado 文件 ( code/ado ) : 包含专门为此项目编写的 ado 文件。例如,对于本文提到的健康不平等项目,作者专门编写了一个用来生成预期寿命的 ado 文件。
  • SSC 上下载的 ado 文件 ( code/ado_ssc ) : 包括项目中使用到的从 SSC 安装的 ado 文件。例如,对于本文提到的健康不平等项目,作者使用了 binscatter 命令。
    • 为什么不让代码使用者自己安装 SSC 的 ado 文件?首先,SSC 中的 ado 文件可能会进行更新,新版本的 ado 文件可能失效或输出不同的结果,不能保证结果的 可复制性。此外,将 ado 文件存储到项目文件夹中可以让人们轻松在新系统上运行代码,无需特意寻找所需的 ado 文件,从而更加方便易用
  • 地图模板的 ado 文件 ( code/ado_maptile_geo ) : 地图模板的 ado 文件与 SSC 上下载的 ado 文件类似,具有会更新、需另外安装的特点。将地图模板的 ado 文件包含在项目文件夹中可以保证项目具有可复制性且方便易用。

本文的代码可以自动将自己撰写的 ado 文件 ( code/ado ) 与 SSC 上下载的 ado 文件 ( code/ado_ssc ) 加入 Stata 的 ado 路径的最前端,从而使得 Stata 在运行过程中能够自动找到这些 ado 文件的位置 ( 详见 code/set_environment.do 中的 adopath ++ 命令,译者注 ) 。同时,当每次使用到 maptile 命令时,本文都会使用 geofolder() 选项来指明地图模板的 ado 文件 ( code/ado_maptile_geo ) 所在路径。

3.2 数据文件夹

所有的数据被存储在以下两个文件夹:

  • 原始数据 ( data/raw )
  • 派生数据 ( data/derived )

如何区分原始数据与派生数据?可以被代码文件夹中的代码导出的数据被称为派生数据,其它数据即为原始数据。也即,如果删除了代码 ( code ) 和原始数据 ( data/raw ) 文件夹之外的所有内容,之后再运行代码,代码将会运行出所有内容,包括派生数据 ( data/derived ) 文件夹、临时文件 ( scratch ) 文件夹和结果 ( result ) 文件夹的所有内容。原始数据与派生数据的划分与数据的“原始”或“处理”程度无关;原始数据文件夹中的数据可能是较早时候派生的,比如与已发表论文相关的数据集。

所有的数据都应放在具有描述性命名 ( 即对数据内容有简要说明 ) 的原始数据 ( data/raw ) 和派生数据 ( data/derived ) 的子文件夹中。

原始数据 ( data/raw ) 文件夹中的每一个子文件夹都应该包含一个名为 source.txt 的文件,以说明数据的获取方式与获取日期。如果是公共数据,描述应该详细到陌生人可以按照提示找到对应数据;如果是非公开数据,描述应该详细到新的合作者可以在不联系您的情况下找到对应数据。例如,非公开数据的 source.txt 可以这样写:“Raj Chetty 在 2015 年 8 月 19 日通过电子邮件从 Emmanuel Saez 收到了 elasticities.dta。该文件来自 Saez 的论文“应税收入的弹性:证据和影响” ( 2002 年 ) 。 ”

3.3 临时文件夹

在一个项目的过程中,我们将产生许多分析,例如大量的表格和数字。当项目结束时,其中的一些将作为结果 ( 如“图 6B”或“表 2” ) 。但在此之前,它们将被称为“预期寿命最低四分位地图”或“预期寿命前 10 大通勤区列表”。这是我们的临时工作,它将位于临时文件夹中。

临时文件夹中的所有内容都存储在描述性命名的子文件夹中,以便随时查看。如何确定数据集属于派生数据 ( data/derived ) 还是临时文件 ( scratch ) ?如果数据集仅用于查看或者它是分析中的临时步骤,则应该被划分为临时文件。如果数据集要被代码再次使用,则应该被划分为派生数据 ( data/derived ) 。

临时文件夹中的所有文件名都应该是描述性的。文件名指定文件包含的内容 ( 例如:一季度预期寿命水平相关性.png 或 Q1 LE levels correlations.png ),而不是它在论文中出现的位置(例如:图 7.png 或 Figure 7.png ) 。

3.4 结果

将在论文中汇报的图与表格存储在结果 ( results ) 文件夹中。 本文使用一个 do 文件 ( code/assign_fig_tab_numbers.do ) 将文件从临时 ( scratch ) 文件夹移动到结果 ( results ) 文件夹中,并重命名。例如,这个 do 文件会复制临时 ( scratch ) 文件夹中的 scratch/Correlations with CZ life expectancy/Q1 LE levels correlations.png结果 ( results ) 文件夹,并重命名为 results/Figure 7.png

需要注意的是, code/assign_fig_tab_numbers.do 应该是所有代码中唯一一个可以在结果 ( results ) 文件夹中创建文件的 do 文件。如果在编辑或修订过程中需要对于图或表格进行重新编号时,只需要修改 code/assign_fig_tab_numbers.do 。如果有人需要找到某个特定图或表格来自何处,这个 do 文件也可以作为一个简单的参考。

3.5 回顾:数据如何在文件夹中流动

项目中主要包括五个主要文件夹:

  • 代码 ( code )
  • 原始数据 ( data/raw )
  • 派生数据 ( data/derived )
  • 临时文件 ( scratch )
  • 结果 ( results )

当运行存储于代码 ( code ) 文件夹中的代码时,数据在其它文件夹中的流动方向为: 原始数据 ( data/raw ) --->派生数据 ( data/derived ) --->临时文件 ( scratch ) --->结果 ( results )

对每个文件夹的简要介绍:

  • 代码 ( code ) 文件夹应当包含所有代码,包括一些子文件夹和三个特殊的文件夹:自己撰写的 ado 文件 ( code/ado ) 、SSC 上下载的 ado 文件 ( code/ado_ssc ) 与地图模板 ado 文件 ( code/ado_maptile_geo ) 。
  • 原始数据 ( data/raw ) 文件夹应当包含项目所需的所有原始数据的文件夹。每个子文件夹中应当包含一个名为source.txt的文件,以说明数据的来源。原始数据的选择应当满足以下条件:当删除除了代码 ( code ) 文件夹与原始数据 ( data/raw ) 文件夹之外的所有文件夹后,运行代码可以重新生成其余文件夹。
  • 临时文件 ( scratch ) 包含运行代码得到的数据的子文件夹。这些数据集随后会被代码调用以生成结果文件。
  • 结果 ( results ) 包含准备在论文中汇报的结果,通常为编号的图或表 ( 例如:图 7,表 1 ) 。

4. 代码风格

4.1 简短、独立、专用的 do 文件

项目中的每个 do 文件都应该满足简短、独立、专用的特点,即每个 do 文件都有一个可以用一句话解释的用途。每个特点的具体要求如下:

  • 简短。一个 do 文件通常应该少于 250 字,几分钟内即可完成阅读并完全理解。当然,250 字并非铁律,do 文件拆分的主要依据还是代码的专用性与易读性。
  • 独立。每个 do 文件仅与其加载的文件和其保存的文件与其它代码交互。也即编辑一个 do 文件通常不会影响其它代码的结果。
  • 专用。一个 do 文件仅有唯一用途。这可以帮助读者更好理解每个 do 文件在代码中的作用。

这样的代码风格可以使得修改代码不再那么令人生畏、耗时且容易出错

当然,上述包含多个 do 文件的代码风格也存在缺点,即您需要追踪以下问题:

  • do 文件的运行顺序
  • 不同 do 文件的交互
  • 在更改某些内容后,应当重新运行哪些代码以更新结果

幸运的是,上述问题可以通过自动构建系统 ( automated build system ) 解决 ( 详见本文第五部分,译者注 ) 。

4.2 指定根目录

根目录即包含代码 ( code ) 、原始数据 ( data/raw ) 、派生数据 ( data/derived ) 、临时文件 ( scratch ) 和结果 ( results ) 五个子文件的文件路径。每个 do 文件都应当能够获取根目录。然而,每个人电脑路径不尽相同,因此直接在 do 文件中绝对路径是不恰当的。相反,它应该被添加到您电脑的 Stata 副本的配置中,并在代码中使用相对路径。具体步骤如下: ( 具体实现案例与可能遇到的问题,详见本文附录部分,译者注 )

  • Step1:打开 Stata,运行 doedit "c(sysdir_personal)'/profile.do 命令,以编辑个人文件夹中的 profile.do 文件。
  • Step 2:运行 global mortality_root "<path_to_mortality_root_folder>" 命令并保存 do 文件。这使得你的电脑中的$root 被设定为<path_to_mortality_root_folder>。
  • Step 3:重启 Stata,您应该可以在结果窗口看到 “running [...]/profile.do” 语句。
  • 检查是否设置成功:运行 ls "$mortality_root/" 命令,结果栏中应当包含代码 ( code ) 、原始数据 ( data/raw ) 等文件夹名称,这说明设置成功。

在代码编写过程中,你可以使用相对路径引用。例如:

use "$root/data/derived/le_estimates/cz_leBY_gnd_hhincquartile.dta", clear

$root 代表您电脑设置中的相对路径,也即在其他人的电脑中不一样。

4.3 do 文件的开头

每一个 do 文件都应该以以下代码开头:

* Set $root
return clear
capture project, doinfo
if (_rc==0 & !mi(r(pname))) global root `r(pdir)'  // using -project-
else {  // running directly
    if ("${mortality_root}"=="") do `"`c(sysdir_personal)'profile.do"'
    do "${mortality_root}/code/set_environment.do"
}

这些代码保证每个 do 文件既可以单独运行,也可以在项目中与其它 do 文件一起运行(通过 project, build 实现)。它所做的最重要的事情是确保 $root 正确定义全局。这些代码的具体作用如下:

  • 首先清空所有返回值 ( return clear ) ,并且报告正在运行的项目 ( capture project, doinfo )
  • 如果存在正在运行的项目,我们将根目录设置为此管线主 do 文件 ( 例如:mortality.do ) 所在的目录。
  • 如果没有正在运行的项目:
    • 首先运行 Stata 个人路径中的 profile.do ,也即您定义为 $mortality_root 的路径
    • 然后运行 code/set_environment.do 。该 do 文件首先将 $root 的值设置为 $mortality_root ,并将 code/adocode/ado_ssc 添加至 Stata 的 adopath ( 从而使得 Stata 能够在运行时自动找到这些 ado 文件的位置 ) ,同时可以暂时禁止 project 命令

除了上述八行代码以外,典型的 do 文件也会包含以下代码 ( 或其中的一部分代码 ) :

* 设置您需要使用的全局变量
global derived "${root}/data/derived"

* 根据系统设置输出图片的格式
* c(os)会返回该电脑的系统,可能的返回值包括"MacOSX", "Unix", or "Windows"
if (c(os)=="Windows") global img wmf //如果是Windows系统,输出wmf格式 ( 将全局变量 img 设为 wmf ) 
else global img png //如果是其它系统,输出 png 格式(将全局变量 img 设为 png )

* 创建需要的文件夹
cap mkdir "${root}/scratch/Correlations with CZ life expectancy"
cap mkdir "${root}/scratch/Correlations with CZ life expectancy/data"

* 删除临时数据
cap erase "${root}/scratch/Correlations with CZ life expectancy/Reported correlations.csv"

* 导入配置设置
project, original("$root/code/set_bootstrap.do")
include "$root/code/set_bootstrap.do"

/*** Estimate CZ-level correlations between life expectancy (levels and trends)
     and local covariates.
***/

第一部分,设置全局变量。这一部分代码会定义在 do 文件中您可能需要使用的全局变量。在这一 do 文件中使用到的全局变量必须在同一个 do 文件中声明 ( 除了根目录 $root ,因为已经在前八行声明过了 ) 。这是因为 project 命令会在运行每个 do 文件时清除全局变量,所以实际上每个全局变量仅仅在本 do 文件中有效。但是如果您交互式地运行 do 文件的片段,全局变量将保存在内存中,这在开发和调试期间通常很有用。

第二部分,创建所需文件夹。这一部分代码会创建需要写入的文件夹。如果尝试写入不存在的文件夹,程序将崩溃。因此,在 do 文件 中自动创建文件夹可确保代码即使在第一次运行或删除了过去创建的文件夹的情况下都能正确运行。 cap 命令表示如果该文件夹不存在时,则使用 mkdir 命令创建新文件夹。

第三部分,删除临时数据,以保证每次运行 do 文件的时候都重新计算。

第四部分,导入配置设置。比如本文使用 set_bootstrap.do 定义了迭代次数全局变量。通过从单个文件中导入这些设置,而不是在每个 do 文件的顶部定义,可以确保设置在不同 do 文件中始终保持一致,并且在需要改变设置时只需在单个文件中修改即可。

* 示例:set_bootstrap.do内容如下
* 迭代次数设置为1000次
global reps 1000
* 样本数设置为625000
global splitobs 625000

最后一部分,注释部分。注释由 /*** 开头,由 ***/ 结尾,简要解释了 do 文件的用途。此注释旨在让尚未阅读 do 文件的人快速了解其用途。

4.4 使用描述性的变量和文件夹名称

变量名称应该能够清楚地表明变量的含义,这能够为之后阅读代码的人节约更多事件。试图通过缩短变量名来节约写代码时间是不合算的。

例如,您估计了一个模型,并将截距和斜率保存成变量,不要将其命名为 a 和 b,而是应当命名为intercept ( 截距 ) slope ( 斜率 ) 。进一步的,如果这两个参数是由 Gompertz 模型得出,这两个变量可以命名为gomp_interceptgomp_slope

Gentzkow 和 Shapiro 为选择变量名称提供了以下经验法则:

  • 默认情况下,变量、函数、文件等的名称应该由完整的单词组成。仅在您确信不熟悉您的代码的读者能够理解并且没有歧义时才使用缩写。大多数经济学家会理解 “income_percap” 是指人均收入,因此无需写出 income_percapita。但是根据上下文的不同,income_pc 可能意味着很多不同的东西,因此不是恰当的变量名。st、cnty 和 hhld 之类的缩写变量名只要在整个代码中含义保持一致也是可以使用的。但是使用 blk_income 来表示人口普查区块中的收入可能会令人困惑,因此不是恰当的变量名。

  • 避免多个变量具有相似含义的情况。比如名为 state_level_analysis.dostate_level_analysisb.do 的 do 文件,或名为 x 和 xx 的变量。

如果仅从变量名不能清晰解释其内涵时,可以使用 label 命令为变量添加描述性的标签。例如,您可能有一个名为 age 的变量的年度数据,该变量的标签为“12 月 31 日的年龄”。

综上,如果变量名称有歧义,应当使用更好的变量名称,并更新所有代码。至少,也可以给变量一个描述性标签。

文件与文件夹的命名也是同理。以下列出一些例子:

  • 图的命名
    • c_1.png 是不恰当的文件名。
    • correlations.png 是不恰当的文件名,如果您使用了不止一次的相关性分析。
    • Q1 LE levels correlations.png 是一个很好的文件名。
  • do 文件的命名
    • med_v_ext.do 是不恰当的文件名。
    • Decompose mortality into medical v external causes.do 是一个好的文件名。
  • 文件夹的命名
    • data/raw/intercensal pop 是不恰当的文件夹名。
    • data/raw/Census County Intercensal Estimates 2000-2010 是一个好的文件夹名。

通过描述性地命名文件和文件夹,我们能够在不阅读代码的情况下查找内容。一个命名良好的文件可以在无需重命名的情况下附加到电子邮件中,收件人也能很好地了解文件内容。

4.5 确保数据集具有独特 ID,并使用 ID 命名文件

我们创建的每个数据集通常都应该有一组经过深思熟虑的独特 ID 变量。通俗地说,我们经常称其为数据的“级别”:一个数据集具有“个人级别”数据,另一个具有“年份级别”数据数据,另一个有“国家级”数据。

例如,本文有一个按区域、性别和家庭收入四分位数划分的预期寿命数据集。对于该数据集,变量 cz、gnd、hh_inc_q 是一组独特的 ID 变量,也即区域-性别-家庭收入四分位数是数据集的“级别”。

了解数据集的独特 ID 是非常重要的,不仅能够表明数据集描述的内容,也能表明其如何与其它数据集合并。

具有独特 ID 的数据集具有以下特征:

  • 没有具有相同 ID 值的重复样本
  • 独特 ID 没有缺失

可以使用 isid 命令检验上述两个特征是否成立,例如 isid cz gnd hh_inc_q 。当允许缺失值时,也即放松特征 2 时,可以使用 isid cz gnd hh_inc_q, missok

将独特 ID 作为文件名通常是有意义的,尤其是需要存储多个聚合级别的数据的时候。比如本文的预期寿命文件夹包含以下数据集,您可以从文件名轻松看出数据集在什么级别上进行聚合。例如, cty_leBY_gnd_hhincquartile.dta 数据集的一组独特 ID 是 cty、leBY、gnd 和 hhincquartile。

cty_leBY_gnd_hhincquartile.dta
cty_leBY_gnd.dta
cz_leBY_gnd_hhincquartile.dta
cz_leBY_gnd.dta
cz_leBY_year_gnd_hhincquartile.dta
national_leBY_gnd_hhincpctile.dta
national_leBY_year_gnd_hhincpctile.dta
st_leBY_gnd_hhincquartile.dta
st_leBY_gnd.dta
st_leBY_year_gnd_hhincquartile.dta

本节的拓展内容可以参考 Gentzkow 和 Shapiro 文章的第五章

4.6 将假设条件写入程序

您编写的代码通常依赖于数据的特征。比如,在您的数据集中 x 是非负数,因此您的代码直接对其取对数而不会造成样本数量的损失。但是,数据的情况可能会在未来发生变化,而这些变化可能造成意想不到的后果。因此在写代码时,最好明确使用 assertisid 等命令将假设条件写入代码。例如:

* Full sample regression of y on log(x)
assert x>0 //不满足 x>0 条件时报错
gen log_x=log(x)
reg y log_x
* Collapse away income dimension
isid age gnd hh_inc_pctile //不满足独特ID时报错
collapse (sum) pop_*, by(age gnd)

这些命令使得代码在不满足前提条件时报错,从而有助于避免问题。此外,这也使得代码更易阅读,读者能够更好了解每一步骤需满足的前提条件。相比之下,写注释的方式,例如 * x is always positive! ,则会令读者花费更多时间检查前提条件是否满足,因此是不恰当的。

需要特别注意的是,在使用 merge 命令进行数据集合并时,应当使用 assert()assert() keep() 选项来明确期望的合并结果。这是因为合并特别容易以意想不到的方式发生错误,我们希望在错误发生时得到通知。知道预期合并结果对于写代码的人也是很有益的。

那么我们的程序中应当包含哪些前提条件?头脑风暴所有可能出错场景是没有必要的,前提条件的价值取决于:

  • 违背前提条件的可能性有多大?
  • 一旦前提条件被违背,结果有多糟糕? ( 例如导致其它代码崩溃、回归系数改变 )

每当您遇到代码出错并修复错误时,请编写测试以保证该错误不会再未来再次出现。

4.7 避免重复代码

重复代码难以阅读和维护,因此容易出现隐藏错误。因此,当您需要您的代码重复执行相同的操作时 ( 对于多个组、多个样本等 ) ,您应该编写一个循环或一个函数,而不是复制代码来做同样的事情。

例如,假设您已经编写了代码来估计和生成对种族调整的预期寿命 ( le_raceadj ) ,现在您需要为未经调整的预期寿命 ( le_unadj ) 生成相同的数字。复制和粘贴代码很诱人,只需将原代码中所有的 le_raceadjto 替换成 le_unadj 。但是复制代码有严重的缺点:

  • 容易遗漏。例如您需要计算结果变量的平均值,但您忘记切换变量名称。代码看起来几乎是正确的并且运行时没有错误,但会产生错误的结果。
  • 未来如需修改将非常困难。无论您或其他人何时更改程序,都需要在多个地方应用该更改。这样做需要更长的时间,而且很容易遗漏需要更改的代码,也很难验证所有代码都已正确更改。
  • 带来调试时阅读的困难。在阅读和调试重复代码时,要验证每个段落之间的不同之处既困难又耗时。“这个 20 行代码块与这个几乎相同的 20 行代码块有什么不同?” 找出答案的唯一方法是逐行浏览它们并仔细比较每一行。

什么时候应当避免代码重复?

重复并不完全是坏事,甚至像“不要重复自己”这样的好习惯有时也过分了。重复代码在以下情况中会变得很麻烦并且容易出错:

  • 重复代码很长
  • 重复很多次
  • 在多个代码文件中重复

把一个简单的步骤写两次是完全合理的:

xtile incdecile_m = income if gender=="m", nq(10)
xtile incdecile_f = income if gender=="f", nq(10)

但是如果你要重复同样的步骤 15 次,你应该写一个循环:

forvalues y=2001/2015 {
	xtile incdecile_`y' = income if year==`y', nq(10)
}

如果这个过程涉及很多行代码,你应该使用一个循环,即使它只重复两次:

foreach g in "m" "f" {
	assert income >= 0 if gender=="`g'"
	xtile incdecile_`g' = income if gender=="`g'" & income>0, nq(10)
	replace incdecile_`g' = 0 if gender=="`g'" & income==0
	label var incdecile_`g' "Deciles of positive income"
}

如何避免重复代码

我们通常使用以下三种方式避免重复代码:

  • 当只需要改变代码中一个变量时,可以使用循环。 foreachforvalues 命令会执行循环,并在每次循环时改变一个变量的值。
  • 当需要改变代码中的许多变量时,可以在 do 文件中使用 program 定义一个程序。然后,您可以在调用程序时传递参数以指定多个变量的值。具体语法可以参考 help syntaxprogram 具体应用可以参考原文代码 code/raceshares/national_racepopBY_year_age_gnd_incpctile.do 与 code/raceshares/national_racepopBY_year_age_gnd.do。
  • 当您需要跨越多个 do 文件执行重复代码时,可以在 ado 文件中定义一个程序,并将其放在 code/ado 路径下。ado 文件的文件名必须与其定义的程序名称相同。例如,原文作者编写了名为 compute_racefracs 的 ado 文件,将其存储在 code/ado 路径下,并在多个 do 文件中调用。注意需要将 code/ado 文件添加至 Stata 的 adopath 中。

当然,您不需要从一开始就决定编写循环、程序还是 ado 文件。您只需要自然地编写程序,当代码需要重复时,将其转换成一个循环;当代码在每次重复时需要改变多个变量,将其转换为程序;当该程序在多个 do 文件中都有用时,将其转换为 ado 文件。

一些技巧

  • 当重复代码间有部分不同的地方,使用 if / else if / else 。这些语句可以使得编程者更容易看出重复代码间的不同之处。
  • 在调试循环代码时,可以添加一句手动定义变量的代码,这样可以使得循环只运行其中一部分。例如,下例中的 local g "m" 使得本来遍历 m 与 f 的代码只遍历 m。
*foreach g in "m" "f" {
local g "m"
	...lots of code...
}
  • 当在 do 文件中定义程序时,在 program define <program_name> 之前添加一句 cap program drop <program_name> 。每次运行 do 文件时,这将自动删除并重新定义程序。它允许您在调试时多次运行 do 文件。否则,当你重新运行它时,do 文件 会崩溃,告诉你程序已经被定义。但在 ado 文件中, cap program drop 是不必要的。
  • 在程序的开头写注释来描述程序的作用是一种很好的做法。您应该将此注释放在该 program define <program_name> 行的正下方,以 /*** 开头,以 ***/ 结尾。

4.8 缩进与换行

如果代码缩进良好并且将长行代码分成多行,则代码更容易阅读。关于如何缩进你的代码,以及如何打断长行,有很多约定。许多编码人员对他们使用的约定有着强烈的偏好。

使用缩进和换行符来生成看起来干净的代码的方法不止一种。但值得遵循以下两条准则:

  • 在单个 do 文件中,所有代码都应遵循相同的缩进和换行样式。如果您正在编辑由使用不同风格的人编写的代码,请遵循他们的风格或更新代码以匹配您自己的风格偏好。
  • 当你要与团队中的其他人密切合作时,定期阅读或编辑彼此的代码来确保你的风格满足彼此的需求,从而有效地协作。

4.9 将日期写成 YYYY-MM-DD 格式

当您在代码注释、 source.txt 、文件或文件名中写入日期时,请使用 YYYY-MM-DD 格式,因为它比其他所有格式都好。这是一个全球标准,所有人都可以阅读和书写它,而不会混淆日期或月份是在前,且字母排序相当于时间排序。

4.10 图片的文件类型

原文代码在 Windows 系统输出 .wmf 格式图片,在其它系统输出 .png 格式代码。地图图片是唯一的例外,如果输出 .wmf 格式,文件会过大,因此使用 maptile, savegraph() 命令导出 .png 格式文件。

为了让代码根据操作系统自动切换文件格式,请在 do 开头加上以下代码:

* Set convenient globals
if (c(os)=="Windows") global img wmf
else global img png

然后在后续代码中使用 .${img} 作为拓展名。地图除外,直接使用 .png 作为拓展名。

4.11 将图片中的数据导出到 CSV 文件中

对于论文中的每个图形,我们都会生成一个 CSV 文件,来存储该图像绘制中使用的数据。该数据应该是图中所示内容的直接表示。例如:

  • 对于分仓散点图命令 binscatter ,CSV 文件保存的是图中散点的坐标,而非绘图时使用的原数据。 ( binscatter 是用来绘制条件期望的 Stata 命令,详见连享会主页Stata:分仓散点图绘制-binscatter-binscatter2,译者注 )
  • 对于相关性图形,CSV 文件保存的是估计参数的值及其 95% 置信区间。

我们保存这个 CSV 文档是出于以下目的:

  • 这允许我们检查图片中的数字是否已更改。比较两个图像文件的不同是很棘手的。生成 CSV 后,如果 CSV 发生更改,意味着图片也发生更改。
  • CSV 便于查找图片中的数据。

4.12 输出论文中报告的数字

论文中报告的每个数字都应直接出现在结果文件夹中的 CSV 文件中。

这要求无需进一步计算即可获得论文中报告的数字。例如,如果我们报告一个 p 值,那么 p 值必须直接出现在 CSV 文件中,而不能仅仅汇报系数和标准误差是不够的。

数字可以通过三种方式出现在结果文件夹中:

  • 如果数字直接出现在论文或附录的表格中,则可以从该表格的 CSV 文件中获取。
  • 如果数字绘制在论文或附录的图中,则可以从该图的 CSV 文件中获取。例如,我们报告了 Figure 7 论文正文中描述的相关性的系数和 p 值。尽管 p 值没有直接出现在图中,但它们已存储在与该图关联的 CSV 中。
  • 如果数字没有出现在任何表格或图中,则必须使用 scalarout 命令将其输出到 CSV。
    • code/lifeexpectancy/le_correlations.do 提供了一个很好的例子。在 do 文件的开头,CSV 文件被删除;计算得到 CSV 文件后,它被写入 scalarout 以便在 do 文件中重复使用;在 do 文件的最后,它被使用 project, creates() 命令以加入项目构建。
    • 如果需要四舍五入,可以使用 scalarout, fmt(%9.Nf) 选项。
    • 将此 CSV 文件输出到具有描述性文件名的临时文件夹中。
    • 使用 assign_fig_tab_numbers.do 将 CSV 复制到结果文件夹中,命名为 Reported numbers - <内容的简要描述>

4.13 提交代码修改前的清单

本文代码使用 Github 平台进行存储。这使得每个历史版本的代码都得到存储,并且能够比较新旧版本间的不同。同时这也使得团队协作成为可能,团队成员在不同电脑终端上工作,并将最新版本的代码上传至 Github。

在将修改后的代码上传 Github 之前,应当确保:

  • 如果您添加了新的 do 文件,请确保将其添加到相关管线的主 do 文件。 ( 例如: mortality.do 是分析管线的主 do 文件 ) 。以便新添加的 do 文件在项目构建时运行。
  • 在提交代码前,务必运行 project <管线名称>, build 以构建项目。这样,您可以确保当其他人从存储库中提取您的最新提交时,他们会获得能够运行的代码。当您提交未构建的代码时,软件开发人员将其称为“破坏构建”。
    • 避免“破坏构建”的必要性在于,明确究竟是您的修改还是他人的修改破坏了代码,以便明确谁应当修复代码。
    • 如果有必要提交代码但代码尚未完成,在这种情况下,请确保您在自己的存储库分支中工作,并且在您的分支合并到主分支之前构建工作正常。

5. 自动化构建

5.1 自动化之前

为 Stata 研究项目组织代码的经典方法是将所有代码放在一个 do 文件中。在那种风格中,如何生成结果很清楚:从上到下运行 do 文件。但是,在一个巨大的 do 文件中协作和维护一个大型项目变得异常困难。只有全职编写代码的人才能希望了解整个 do 文件的结构以及所有部分如何组合在一起——即便如此,也相当耗时。您将面临如下的问题:

  • 我需要编辑代码的哪一部分来更新这个结果?
  • 如果我编辑此代码,它将如何影响其余代码和结果?
  • 我是否需要等待数小时才能让代码从上到下运行以获得最新结果?还是仅需运行代码片段?
  • 为什么从上到下运行代码后代码崩溃了?
    • 每一代码片段都运行正常,为什么在一起不能正常运行?
    • 如果我们重新排列代码,会如何影响结果?

因此,将代码拆分为多个 do 文件是很自然的。但是接下来的挑战是弄清楚所有的 do 文件是如何相互关联的。再一次,只有全职编写代码的人才能希望了解整个 do 文件集合的结构以及所有部分如何组合在一起,您仍然会面临相似的问题。

5.2 自动化之后

在任何大型编码项目中,“了解整个 do 文件集合的结构以及所有部分如何组合在一起”并不是人类头脑中要做的工作。它占用了我们的时间,而且我们不是很擅长。相反,我们将通过使用“自动构建系统”将该工作转移到我们的计算机上。这样做对许多研究人员来说是新事物,但我们落后于软件开发人员数十年。经典的构建自动化工具 make 最初是在 1976 年编写的。

我们编写的每个 do 文件不仅简短而且重点突出,而且还是一个独立的模块。协作者可以编辑任何 do 文件,并且知道该代码仅通过它保存的文件影响其他代码和结果。do 文件仅通过它加载的文件受到其他代码的影响。当我们编写 do 文件时,我们将告诉自动构建系统我们的代码加载和保存哪些文件。我们还将使用构建脚本 ( 也称为“主执行文件”或“管线” ) 告诉自动构建系统运行我们的执行文件的顺序。

在告诉自动化构建系统如何运行我们的 do 文件以及每个 do 文件加载和保存哪些文件之后,自动化构建系统可以:

  • 从上到下构建项目。
  • 检查自上次构建项目以来哪些文件已更改,并且仅重新运行依赖于已更改文件的 do 文件。所有结果都会更新,但只有需要重新运行的代码才会重新运行。
  • 两次构建项目,并确认当您重新运行代码时,您会得到相同的结果。这将验证您的结果是可复制的。
  • 反馈项目构建中可能存在的逻辑问题。例如,如果您尝试在文件尚未构建的情况下调用某文件,代码会报错,即使您的电脑中已经存在这一文件 ( 可能是上次运行代码时生成的 ) 。
  • 告诉你每个文件是如何连接在一起的。对于任何文件,它可以告诉您哪个 do 文件 ( 如果有 ) 创建了它以及哪些 do 文件使用了它。因此,您可以追溯文件是如何创建的。或者追踪更改文件将如何影响其他代码和文件。

通过软件处理这些任务,没有一个人需要了解整个 do 文件集合以及所有部分如何组合在一起。您可以专注于您正在处理的 do 文件,这很容易管理,因为它们简短、独立、有唯一的用途。如果您更改正在编辑的 do 文件的输出,您可以查询构建软件以找出哪些后续文件将受到影响。进行更改后,您可以运行构建并知道所有结果都已更新,无需等待重新运行任何不受您的更改影响的代码。

5.3 使用-project-在 Stata 中构建

我们所有的代码都是使用 Robert Picard 为 Stata 创建的自动构建工具 project 运行的。尽管有更通用的自动化构建工具和更强大的自动化构建工具,但对 Stata 使用者而言, project 特别方便。使用自动化构建工具最具挑战性的部分是正确记录代码依赖或创建的所有文件。 project 有助于将这些信息直接记录在您的 do 文件中。

我强烈建议您阅读 help project 以了解 project 功能及其提供的功能的全部细节。我将在这里介绍其重点功能。当您运行诸如 project mortality, build 的构建命令时,项目会检查自上次构建以来哪些文件已更改。然后它会自动运行任何已更改或依赖于已更改文件的 do 文件。例如,如果您更改生成一个数据集的代码,并且另一段代码使用该数据集生成结果,project 将重新运行两者以确保更新最终结果,不依赖这些结果的其他代码将不会重新运行。

构建 ( build ) 命令

为了实现以上功能, project 要求每个 do 文件都指明其加载或生成的相关文件,这可以使用构建 ( build ) 命令实现。每一次加载文件 ( 比如数据集 ) 或生成文件 ( 比如数据集、表格或图片等 ) 时,都应当运行构建命令。构建命令主要有以下三种类型:

  • project, original(filepath) 意味着您正在使用不在此项目中生成的文件。
    • project, relies_on(filepath) 表示您正在引用不在此项目中生成的文件,例如 PDF 或文本文件。您引用的这个文件不会影响代码或结果,您只是将它作为您正在使用的过程或数据的重要参考。在实践中,这与 project, original(filepath) 作用相同。
  • project, uses(filepath) 意味着您正在使用在此项目中生成的文件。
  • project, creates(filepath) 意味着您刚刚创建了这个文件。

默认情况下,这些构建命令中的每一个都会自动清除内存中的数据集。通常这不会有问题,因为您的代码通常看起来像这样:

project, original("$root/data/raw_data/some data.dta")
use "$root/data/raw_data/some data.dta", clear

<do lots of things>

save "$root/data/derived_data/cleaned data.dta"
project, creates("$root/data/derived_data/cleaned data.dta")

注意 project, originaluse 命令之前, project, createssave 命令之后。所以清除加载的数据是无关紧要的。但有时您不希望加载的数据被清除,在这种情况下您需要将 preserve 选项添加到构建命令中。一些常见的情况是:

  • 将数据集合并到加载的数据中
  • 保存后继续使用数据集
  • 在循环中生成许多数字

忽略 preserve 选项是首次运行代码时代码崩溃的最常见原因。在提交前构建项目有助于注意到这个问题。另一种方法是给每个构建命令都添加 preserve 选项,这会减慢构建速度。

构建命令与文件夹的对应关系

在本文介绍的文件夹构建方法下,构建命令与访问的文件夹存在直接关联:

  • 加载数据
    • 原始数据文件夹中的文件使用 project, original 命令加载
    • 派生数据与临时文件文件夹中的文件使用 project, uses 命令加载
    • 结果文件夹中的文件不会通过代码加载
  • 保存数据
    • 派生数据、临时文件和结果文件夹中的文件使用 project, creates 命令生成

实际上,在本文提到的健康不平等项目中,情况会稍微复杂一些,因为该项目中构建通过多个管线来构建。

但是即使您弄混了 originaluses 命令也无伤大雅, project 会报错并指出该错误。

记录对 ado 文件的依赖关系

如果某个 do 文件中运行了在 code/ado 文件夹中的 ado 文件里定义的代码,则称这个 do 文件依赖于这一 ado 文件。例如,当 do 文件中使用了 compute_racefracs ,且 code/ado/compute_racefracs.ado 文件发生更改时,do 文件需要重新运行。因此您需要使用 project, original(code/ado/compute_racefracs.ado) 记录这一依赖关系。

对于 do 文件对 code/ado_ssccode/ado_maptile_geo 文件夹中 ado 文件的依赖关系,不要特别担心。由于该代码不是在项目中编写的,因此不太可能更改。

在实践中,记住为 ado 文件程序的依赖项添加构建命令比记住其他构建命令要困难得多。试着意识到这一点并记住它们。但根据我们的经验,它们有时会被遗忘是不可避免的。因此,如果您正在更新 code/ado 中的程序,通常值得仔细检查所有使用该程序的 do 文件是否声明了它们对 ado 文件的依赖关系。如果程序名称与众不同,您可以通过使用 Atom、Sublime Text 或 Notepad++ 等编辑器在所有 do 文件中搜索程序名称来轻松完成此操作。

除了构建命令以外的功能

大多数情况下,您只使用 projectproject, build 命令。但是 project 也有其它有用的功能:

  • project, list(concordance) 将列出项目使用或创建的每个文件,并在每个文件下列出使用或创建它的 do 文件。这一列表会保存到 archive/list 路径下的文本文件中。这对于跟踪文件的创建方式以及更改的文件可能影响哪些代码和结果很有用。
  • project, replicate 将构建项目,将项目创建的所有文件移动到名为 replicate 的文件夹中,然后再次构建项目并打印一份报告,列出两次构建之间不同的所有文件。
    • 专注于数据集和结果。日志在构建之间可能不同的原因有很多,通常这没什么好担心的。
    • 有时数据集在复制时并不相同,因为存储为双精度 ( double ) 的变量的最后一位存在一些随机性。这通常是因为 collapse 命令基于双精度 ( double ) 。这种情况是无需担心的,但你也可以使用 float()recast float <varlist>, force 命令转换为浮点数 ( float ) 。
  • project, share(<sharename>, alltime nocreated) 将在存档 ( archive ) 路径下创建一个文件夹,其中包含从头开始构建项目所需的所有文件。这在创建复制文件以在线发布时很有用。
    • 如果您运行 project, setup 并指向存档路径下的主 do 文件,您应该可以通过运行 project, build 在存档路径下构建项目。但您也可能发现有一些依赖项没有使用构建命令声明,此时您需要在 do 文件中补充声明。

再次建议阅读 help project 以了解 project 的全部功能。

6. 附录:如何复现上述项目?

Github 仓库地址:https://github.com/michaelstepner/healthinequality-code

  • 数据下载。代码数据下载地址如超链接所示。在本文的示例中,根目录为"D:/example",也即下载的代码与数据被放置于"D:/example"文件夹下,您可以选择任意路径。需要注意的是,请将下载的代码与数据文件夹中的文件而非整个文件夹放置进根目录,也即"D:/example"下包括 code、data 和 scratch 等文件夹,而非 healthinequality-code-main 和 health_ineq_replication_dataonly 文件夹。

  • 命令行输入 sysdir ,查看您电脑中 Stata 的 PERSONAL 路径

  • 运行 doedit "c(sysdir_personal)'/profile.do"` ,这会弹出一个 do 文件编辑器

  • 在 do 文件编辑器中写入 global mortality_root "<path_to_mortality_root>" ,其中 <path_to_mortality_root> 是您想指定的根目录。以根目录选取为"D:/example"为例,则该语句为 global mortality_root "D:/example"

  • 保存 do 文件。如弹出设置文件存储路径选项,将其取名为 profile.do,存储至 PERSONAL 路径下。

  • 重启 Stata,如果上述操作成功,结果窗口应该有一行字"running [...]/profile.do"

  • 命令行输入 ls "$mortality_root/" ,您应该可以看到代码与数据文件,这说明配置成功

  • 命令行输入 project, setup ,Stata 会弹出一个选项框。文件路径选择"D:/example/mortality.do"。如果您需要 text log 文件,而非 SMCL log 文件,勾选选项框。最后点击 OK。

  • 再次运行 project, setup ,此次文件路径选择"D:/example/mortality_datagen.do"

  • 运行 project mortality_datagen, build 。原文重复次数设置为 1000 次,如果您直接运行可能导致运行速度较慢。可以在"D:/example/code/set_bootstrap.do"中将 global reps 1000 改成更小的数,这可以使得运行速度加快。

7. 拓展阅读:推介若干顶刊在 Github 上的复现仓库

《美国经济评论》The American Economic Review ( AER )

  • Angelini, P., & Generale, A. 2008. On the evolution of firm size distributions. American Economic Review, 98(1), 426-38. -Link-, -PDF-, -Github-replication-

  • Farber, H. S., Rothstein, J., & Valletta, R. G. 2015. The effect of extended unemployment insurance benefits: Evidence from the 2012-2013 phase-out. American Economic Review, 105(5), 171-76. -Link-, -PDF-, -Github-replication-

《经济学季刊》Quarterly Journal of Economics ( QJE )

  • Kogan, L., Papanikolaou, D., Seru, A. and Stoffman, N., 2017. Technological innovation, resource allocation, and growth. Quarterly Journal of Economics, 132(2), pp. 665-712. -Link-, -PDF-, -Github-replication-

  • Coppola A, Maggiori M, Neiman B, et al. 2021, Redrawing the map of global capital flows: The role of cross-border financing and tax havens[J]. The Quarterly Journal of Economics, 136(3): 1499-1556. -Link-, -PDF-, -Github-replication-

连享会代码复现仓库:

8. 参考资料

9. 相关推文

Note:产生如下推文列表的 Stata 命令为:
lianxh 重现 复现
安装最新版 lianxh 命令:
ssc install lianxh, replace

相关课程

免费公开课

最新课程-直播课

专题 嘉宾 直播/回看视频
最新专题 文本分析、机器学习、效率专题、生存分析等
研究设计 连玉君 我的特斯拉-实证研究设计-幻灯片-
面板模型 连玉君 动态面板模型-幻灯片-
面板模型 连玉君 直击面板数据模型 [免费公开课,2小时]
  • Note: 部分课程的资料,PPT 等可以前往 连享会-直播课 主页查看,下载。

课程主页

课程主页

关于我们

  • Stata连享会 由中山大学连玉君老师团队创办,定期分享实证分析经验。
  • 连享会-主页知乎专栏,700+ 推文,实证分析不再抓狂。直播间 有很多视频课程,可以随时观看。
  • 公众号关键词搜索/回复 功能已经上线。大家可以在公众号左下角点击键盘图标,输入简要关键词,以便快速呈现历史推文,获取工具软件和数据下载。常见关键词:课程, 直播, 视频, 客服, 模型设定, 研究设计, stata, plus, 绘图, 编程, 面板, 论文重现, 可视化, RDD, DID, PSM, 合成控制法

连享会小程序:扫一扫,看推文,看视频……

扫码加入连享会微信群,提问交流更方便

✏ 连享会-常见问题解答:
https://gitee.com/lianxh/Course/wikis

New! lianxh 命令发布了:
随时搜索连享会推文、Stata 资源,安装命令如下:
. ssc install lianxh
使用详情参见帮助文件 (有惊喜):
. help lianxh