谢作翰 [译] | (知乎 | 简书 | 码云)
Stata连享会 精彩推文1 || 精彩推文2
编者按: 这是 Stata Blog 上的一篇推文,由浅入深地讲解了如何从一个简单的任务出发,逐步通过撰写 dofile,adofile 来实现任务的自动化可重复性执行。这篇推文帮助我们在不自觉中学会了编写标准的 Stata 程序 (ado 文件)。
原文地址 :https://blog.stata.com/2018/10/09/how-to-automate-common-tasks/
连享会计量方法专题……
高效进行数据分析的关键是将常见任务自动化。自动化能把你从大量重复性操作中拯救出来,不但节约时间,还能减少出错概率。
本篇让我们使用Stata自动执行一些任务。任务本身并不重要,重要的是熟悉自动化执行任务的流程。
我们选择的任务是将变量标准化——减去变量的均值并除以其标准差。
正如您所知道的,Stata社区中有现成的命令来执行此操作,而且比我们接下来自己写更为便捷(在Stata中搜索 normalize variable);您也可以使用Stata的egen命令对单个变量标准化,但我们打算做的远不止如此。
我认为本文读者是Stata自动化任务的新手。所以,如果您已经是专家,可能对这篇文章不感兴趣,当然也可能会一些新的发现。
首先,我们将直接在分析脚本中执行标准化操作。在Stata中,我们将分析脚本称为 do-files,因为它们可以执行某些操作。
让我们把原始变量命名为 x。因为我们不想改变现有变量的内容,所有新建一个变量 xN,其中 N 后缀表示标准化(如果您不喜欢 N 后缀,可以改变,比如 _norm
,也可使用前缀)。Stata 的 summarize 命令将给出原始变量平均值和标准差。
*-----------begin------mydo.do-----
···
summarize x
generate xN = (x - r(mean)) / r(sd)
···
*-----------end--------mydo.do-----
如上,对一个变量标准化只需要两行代码即可。
r(mean) 和 r(sd) 是什么意思,我们又该如何了解它们?
它们分别表示变量均值和标准差的返回值。
在 Stata 中,几乎所有命令都会返回结果。已模型估计为主要目标的命令 (如 regress
, logit
等) 的返回结果以 e() 表示,大多数其他命令以 r() 表示。输入 help summarize
并拉到帮助文件的底部就能看到 summarize
返回的所有结果及其描述。也可以在执行完 summaryrize
命令后输入 return list
命令来查看返回值列表。
既然我们的任务只有两行,那为什么我们要自动化呢?
因为当我们需要大量重复操作时,即使只有两行代码,也极易出错。比如,我们想复制这段代码 100 次进行 100 个变量标准化时,我们必须将代码中变量名 x 更改为 100 个新的变量名称,但是我们往往会忘记。你可能会忘记更改 summarize 中的 x,或是忘记更改 xN,并收到报错消息。或是忘记更改在表达式中的变量名。这三种错误我都犯过。
我们将脚本放入自己的do-file中。
normalize.do (1)
version 15.1
summarize x
generate xN = (x - r(mean)) / r(sd)
ps:在文件顶部我添加了版本命令。请切记,一定要为你的do文件标明版本信息!我使用的是 Stata 15.1,一旦标记上,这个脚本将始终以 15.1 版 stata 的特性运行,即使将来用 Stata 42 版运行这个文件(可能 42 版的 stata 早已取消 summarize
命令或完全改变 summarize
的工作方式)但 Stata 会识别出版本号,并按 15.1 版本的语法规则正常运行。
我们通过输入以下命令执行所写的脚本
. do normalize
或者在直接在do文件里添加 do normalize 语句。
我们目前的 normalize.do 并不太有趣。我们需要它来处理除 x 之外的变量。
这是一个版本:
normalize.do (2)
version 15.1
summarize `1'
generate `1'N = (`1' - r(mean)) / r(sd)
然后输入
. do normalize y
从(1)到(2)的变化是什么?我们所做的只是用 `1′
替换每次出现的 x 。为什么是 `1’ ?Stata 的 do-files 会将其参数依次放进编号为 1, 2, 3 等的局部暂元进行解析。第一个参数进入局部暂元`1'
, 第二个参数进入 `2'
,依此类推。
什么是局部暂元?暂元是一个存储价值的容器。1 可以是暂元名称。为什么我们用`'
包裹 1?因为如果我们只输入 1,那指的是数字 1,而我们需要的是小盒子(暂元) 1 中的值,所以我们需要对它解引用。因为我们输入了 y ——我们的第一个(也是唯一的)参数,所以暂元**`1’** 解引为(deference)y。如果你不喜欢“解引用”这个表达,也可以说 “1” 扩展为 y 。
若你在我们的第二版 normalize.do 中以 y 代替 `1’ ,它就成了第一个版本。这也正是 Stata 所做的。
使用我们的新 normalize.do,我们可以方便的输入:
. do normalize myvariable
. do normalize myothervariable
. do normalize x1
. do normalize x2
...
. do normalize x100
使用以上代码我们犯错率大大减小了,只是代码非常冗赘。这个问题稍后我们回来解决。
现在我现在想探究的是,能否在我们的 do 文件中加入 Stata 的 if 限定符,答案是肯定的,而且很容易。
. do normalize income if male == 0
我们可能想对样本中的女性对象标准化,这时候就可以使用 if 限定符 if male== 0。
以下是包含 if 和 in 限定符的 do 文件。
(如果你不知道什么是 in 限定词,点击https://www.stata.com/help.cgi?in)
normalize.do (3)
version 15.1
syntax varlist(min=1 max=1) [if] [in]
summarize `varlist' `if' `in'
generate `varlist'N = (`varlist' - r(mean)) / r(sd) `if' `in'
最后两行代码与之前版本相比有所改变,主要在两个方面:
`varlist’
;if
、in
限定符。我们的 do-file 现在直接支持 if 和 in 限定符,所以新的 syntax 命令似乎表现出很多魔力,事实上确实如此。这里的 syntax 指令具有神奇的作用,具体解释如下:
_varlist
中_),可选的 if 限定词,可选的 in 限定词,除了缺少选项部分(option
)其他都有了。前两个 do 文件有个问题我们此前忽略了——我从未检查过 `1’ 是一个未缩写的变量名。Stata 允许缩写变量名称。如果你有一个名为 foreign 的变量,并且没有其他变量名的缩写是 for,此时输入
. do normalize for
便会会创建一个名为 forN 的新变量,而不是 foreignN。无论如何,你必须要小心。一些方法可以解决这个问题,但我们不再过多介绍。
使用 syntax 语句则没有这个问题。即使在命令行中输入 for,局部暂元 `varlist’ 也会扩展为未缩写的变量名 foreign。这也是 syntax 的过人之处。
现在,让我们来解决代码冗赘问题。
如果我们想要标准化的变量数量很多怎么办?这也很容易,但我们必须最终添加到我们的两行计算代码中。
这是一个 do-file,它接受变量列表并对每个变量进行规范化,同时支持 if 和 in 限定符。
normalize.do (4)
version 15.1
syntax varlist [if] [in]
foreach var in `varlist' {
summarize `var' `if' `in'
generate `var'N = (`var' - r(mean)) / r(sd) `if' `in'
}
具体解释如下:
`varlist'
解析为 do 所指定的变量列表。`varlist'
仓库中存放的是由环境中多个变量名组成的列表,而仓库 `var'
存放的是某单个变量名,并且var这个仓库里存放的变量名会随着循环次数的变化改变,但每次都只能存放一位,一直到遍历仓库 `varlist`
中的所有变量名。我们现在输入
. do normalize x1 x2 x3 if male==0
或者
. do normalize x*
normalize.do 将从 Stata varlist
中接受符合条件的变量。如果您还了解 varlist
,请单击 https://www.stata.com/help.cgi?varlist 查看所有含义。
连享会计量方法专题……
这小小的自动化过程给我们带来了许多方便,我们可能不满足仅仅把它保存为一个 do 文件,而想要将它变成一个新的 Stata 命令,这样我们可以在任何项目中使用它,甚至可以分享给我们的同事。
再说一次,这并不难。
我们将创建一个 ado 文件(自动化的 do 文件)。在 ado 文件中定义的程序就像Stata内置命令一样,会自动被发现并运行。
normalize.ado (a)
capture program drop normalize
program normalize
version 15.1
syntax varlist [if] [in]
foreach var in `varlist' {
summarize `var' `if' `in'
generate `var'N = (`var' - r(mean)) / r(sd) `if' `in'
}
end
我们做了哪些调整呢?
我们现在有一个程序,只要我们输入 normalize ,Stata 就会自动发现并运行它。
我们现在可以输入
. normalize x1 x2 x3 if male==0
或
. normalize x*
我们可以将文件 normalize.ado 提供给同事,他们可以像使用其它 Stata 命令那样使用它。
现在行动起来,自动化执行自己的任务吧!
adopath
可以查看路径清单。你可以使用 adopath + pathname
命令添加新的路径至清单中。A. ado 文件的保存
如果您将 normalize.ado 放在可以被 Stata 发现的路径中(如当前的工作目录)它的确足以应付日常的自动化需求。但您可能不希望将其放在每个工作目录中。而且万一我们对命令改动升级了怎么办?我们还得在好几个地方改变它。这时,在 Stata 中输入
. adopath
该路径上的一个目录将被标记为 (PERSONAL)。复制 normalize.ado 到此处。无论您在哪个目录中工作,现在都可以在所有项目中找到它。
如果您将 normalize.ado 提供给同事,请告诉他们将其复制到他们的 (PERSONAL) 目录。
当然,你也可以把你的 ado 文件放在别的地方,但此时需要使用 adopath + pathname
把路径名称告诉 Stata,它会把这个路径添加到其查找 ado 文档的路径清单中。详情参见 help adopath
。
事实上,我并不总是采用自动化。我发现不同情况下止步于我们的 do 文件的版本 (1), (2), (3) 或 (4) 都是有用的。或者一直到新的命令。
另外,我们将程序名叫做 normalize 并将其放入名为 normalize.ado 文件中并非随意:程序名称和文件名必须相同!
B. 程序的调试
还有一个细节。每次键入 do normalize …时,都会从 normalize.do 文件重新加载您的文件。键入 normalize 后,您的ado文件程序将保留在 Stata 的内存中。下次键入 normalize 时,Stata 会从内存运行程序而不重新读取 normalize.ado 文件。那更快。但是…如果您正在调试程序并编辑文件,则不会重新加载您的更改。在键入 normalize 之前,您需要键入 discard … 这样,您的程序将从内存中删除,并将从您的文件重新加载。更为标准的做法是,在程序的首行写入 capture program drop normalize
,以便在调试过程中自动清除内存中的旧版程序。当完成程序的所有调试工作后,可以去掉这一行,以便提高程序的运行效率。
在调试程序过程中,有些莫名的错误往往难以觉察,此时,可以使用 set trace on
(官方命令) 或 help tr
(外部命令) 来呈现程序的解析过程。Stata 会在出错的地方自动停下来,并用红色标记出错信息。
C. 程序的分享和发布
与整个 Stata 社区分享您的新命令也很简单。查看常见问题解答如何与 Stata 用户共享新命令?
这是一个典型的自动化过程:
恭喜,您现在可以自动执行 Stata 中的常见任务。无论你是否愿意,你都在成为一名程序员。
如果你对我们迄今为止所做的事情感到满意,这将是退出阅读的好时机。
您可能不希望自动将 N 附加到原始变量名称的末尾以指定标准化变量。也许你想使用不同的字母,或者可能是一组字符,比如 _norm。或者您可能更喜欢后缀的前缀。甚至也许你想要两个。
我们可以通过增加选项实现这一点。
normalize.ado(b)
program normalize
version 15.1
syntax varlist [if] [in] [ , prefix(name) suffix(name) ]
foreach var in `varlist' {
summarize `var' `if' `in'
generate `prefix'`var'`suffix' = (`var' - r(mean)) / r(sd) `if' `in'
}
end
从版本(a)到版本(b),我们所做的只是把
syntax varlist [if] [in]
改为
syntax varlist [if] [in] [, prefix(name) suffix(name)]
并改变
generate `var'N = ...
至
generate `prefix'`var'`suffix' = ...
让我们先来理解 syntax 行的变化:
方括号 [ ] 表示所含部分为可选项 (并非强制设定)。
但是,如果用户决定使用这些选项,他们必须先输入逗号 ,
。
然后,他们可以键入 prefix() (前缀),或 suffix() (后缀),或两者兼具。
如果他们键入 prefix() (前缀),那么局部暂元 prefix
将包含他们在括号中输入的内容。
我们编写 syntax 命令时必须小心。因为我们写了 prefix(name) 而不是 prefix(string),所以用户必须在括号中键入符合 Stata 变量名规则的字符串,而不能随意填写。
简言之,`prefix'`var'`suffix'
构成的新变量名必须符合 Stata 的命令规则 (详见 help varname
)。
那么,以下代码是什么意思呢?
generate `prefix'`var'`suffix' = ...
暂元 `prefix’ 和 `suffix’ 将扩展为用户在 prefix 和 suffix 选项中输入的任何内容。我们的新变量名将带有用户输入入的前缀和后缀。
使用我们的新 ado 文件,我们现在可以输入类似的内容
. normalize x1 x2 x3 x4 , prefix(norm_of_)
. normalize x* , prefix(norm_of_)
避免该错误的一种方法是默认为新变量添加后缀 “N”,这可以通过在 syntax 行下面添加以下三行新的语句来实现:
if "`prefix'`suffix'" == "" {
local suffix "N"
}
其含义是:如果前缀和后缀 (`prefix’ 以及 `suffix’) 都为空,则使用 “N” 作为后缀。
另一个不错的改进是为我们的新变量添加标签,例如:
label variable `prefix'`var'`suffix' "`var' normalized"
我们可以将上述语句添加在 for 循环中中的 generate 命令之后,以便实现对所有变量批量添加标签。
这就是程序变长的方式。您可以改进它们,并添加功能。坚持这一点,你很快就会编写一些复杂代码来难倒你的同事。
关于我们
Stata
或Stata连享会
后关注我们。联系我们
Stata连享会(公众号: StataChina)
,我们会保留您的署名;录用稿件达五篇
以上,即可免费获得 Stata 现场培训 (初级或高级选其一) 资格。Stata连享会 精彩推文1 || 精彩推文2