stata连享会

Stata:编写ado文件自动化执行常见任务

2018-10-22  本文已影响315人  stata连享会

谢作翰 [译] | (知乎 | 简书 | 码云)


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自动化任务的新手。所以,如果您已经是专家,可能对这篇文章不感兴趣,当然也可能会一些新的发现。

1. 使用脚本文件

首先,我们将直接在分析脚本中执行标准化操作。在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,并收到报错消息。或是忘记更改在表达式中的变量名。这三种错误我都犯过。

2. 使用 Do 文件自动化执行

我们将脚本放入自己的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' 解引为(deferencey。如果你不喜欢“解引用”这个表达,也可以说 “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 限定符,答案是肯定的,而且很容易。

3. 为什么我们想要 if 限定语句?

. do normalize income if male == 0

我们可能想对样本中的女性对象标准化,这时候就可以使用 if 限定符 if male== 0

以下是包含 ifin 限定符的 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'

最后两行代码与之前版本相比有所改变,主要在两个方面:

这里的 syntax 指令具有神奇的作用,具体解释如下:

前两个 do 文件有个问题我们此前忽略了——我从未检查过 `1' 是一个未缩写的变量名。Stata 允许缩写变量名称。如果你有一个名为 foreign 的变量,并且没有其他变量名的缩写是 for,此时输入

. do normalize for

便会会创建一个名为 forN 的新变量,而不是 foreignN。无论如何,你必须要小心。一些方法可以解决这个问题,但我们不再过多介绍。

使用 syntax 语句则没有这个问题。即使在命令行中输入 for,局部暂元 `varlist' 也会扩展为未缩写的变量名 foreign。这也是 syntax 的过人之处。

现在,让我们来解决代码冗赘问题。

如果我们想要标准化的变量数量很多怎么办?这也很容易,但我们必须最终添加到我们的两行计算代码中。

这是一个 do-file,它接受变量列表并对每个变量进行规范化,同时支持 ifin 限定符。

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'
}

具体解释如下:

我们现在输入

. do normalize x1 x2 x3  if male==0

或者

. do normalize x*

normalize.do 将从 Stata varlist 中接受符合条件的变量。如果您还了解 varlist,请单击 https://www.stata.com/help.cgi?varlist 查看所有含义。

4. 创建一个新命令:ado 文档

这小小的自动化过程给我们带来了许多方便,我们可能不满足仅仅把它保存为一个 do 文件,而想要将它变成一个新的 Stata 命令,这样我们可以在任何项目中使用它,甚至可以分享给我们的同事。

再说一次,这并不难。

4.1 编写 ado 文件

我们将创建一个 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 命令那样使用它。

现在行动起来,自动化执行自己的任务吧!

4.2 ado 文件注意事项

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 用户共享新命令?

5. 典型流程归纳

这是一个典型的自动化过程:

恭喜,您现在可以自动执行 Stata 中的常见任务。无论你是否愿意,你都在成为一名程序员。

如果你对我们迄今为止所做的事情感到满意,这将是退出阅读的好时机。

6. 添加更多的选项

您可能不希望自动将 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' 将扩展为用户在 prefixsuffix 选项中输入的任何内容。我们的新变量名将带有用户输入入的前缀和后缀。

使用我们的新 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连享会 精彩推文1 || 精彩推文2


欢迎加入Stata连享会(公众号: StataChina)
上一篇 下一篇

猜你喜欢

热点阅读