Stata编程:暂元,local!暂元,local!

发布时间:2021-06-17 阅读 504

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

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

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

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

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

⛳ Stata 系列推文:

PDF下载 - 推文合集

译者: 万莉 (北京航空航天大学)
邮箱: wanli_buaa@163.com

Source: Stata Programming Essentials


目录


1. 引言

Stata Programming Essentials 这篇文章介绍了暂元(macros)在 Stata 编程中的一些常用方法,并结合简单形象的例子为我们处理数据提供了一些启发。本文在翻译原文的基础上,适当做了一些删减和增补。

若你还是 Stata 小白,可阅读本文的姊妹篇 Stata for Researchers 来理解 Stata 的基本语法;若你已熟练使用 Stata,可阅读本文的另一姊妹篇 Stata Programming Tools 来提高自己的编程技能。

当你需要在 Stata 中做重复性工作时,是否会倍感烦恼?暂元 (macros) 的使用能精简我们的代码,大幅度提高我们的工作效率。接下来,让我们一起学习暂元(macros)的使用技巧吧!

2. 什么是暂元 (macros)?

我们可以将暂元理解成一个空的菜篮子(需要给篮子取个小名)。 为了烹饪美食(执行命令),我们需要采购食材(可以是数字,复杂的字符串,表达式等),将其装在菜篮子里。烹饪不同的美食,我们可能需要不同的食材,但可以共用一个菜篮子。当然,我们也可以用新的菜篮子。

我们也可以将暂元比作“手机来电显示”。由于我们设置了联系人名字,所以来电只显示了名字,但这名字背后代表了一串数字号码。

在 Stata 中,暂元分为局部暂元 (local)全局暂元 (global) 两种。其中,局部暂元执行完一次命令后就失效了,而全局暂元在 Stata 关闭之前一直存在,可以随时引用。这种区分与其他编程软件中关于局部变量与全局变量的定义是一样的。注意:运行包含局部暂元的多行命令时,需要一次性执行完,否则会报错。

在大多数情况下,局部暂元的使用就足够满足我们的需求。如果我们需要定义一个可为多个 do 文件共同调用的暂元,那只能使用全局暂元。除非有必要使用全局暂元,使用局部暂元更为稳妥。本文讲着重讲解局部暂元的使用方法。

2.1 基本语法

定义局部暂元的基本语法结构为:

local name value

其中,name 为局部暂元的名称,value 为局部暂元储存的内容。引用局部暂元时,我们需要用到左单引号 (left single quote) 与右单引号 (right single quote),形如 `name'

注意:左单引号和右单引号是不同的。左单引号 ` 位于键盘左上角,在波浪号 ~ 的下方(共用一个键),右单引号 ' 在键盘右边,位于双引号 “ 的下方(共用一个键)。具体位置详见下图:

2.2 存放数字

// case 1
. local x 1
. display `x'
1 

// case 2
. local x 2+2
. display `x'
4

* 上述例子等价于
. local x=2+2
. display `x'
4

// case 3
. local x 2+2
. display "`x'"  
2+2 

Stata 在执行命令时,若看到暂元,会先读取暂元的内容,再将其传给命令,最后输出结果。比如,我们定义暂元 x2+2display `x' 相当于执行了 display 2+2,此时 display 相当于计算器,输出 4;但是 display "`x'" 相当于执行了 display "2+2",此时 display2+2 视作字符串输出。

值得注意的是,虽然 local x 2+2local x=2+2display 命令后的输出结果一样,但仍存在本质上的差异。前者暂元的内容实则为 2+2,而后者会先计算 2+2,然后将 4 赋给暂元。

2.3 存放文字

. local name 好好加油 学习Stata
. dis "`name'" //dis为 display 的缩写
好好加油 学习Stata

2.4 存放表达式

Stata 的暂元处理器可以识别表达式,即可以放在 generatereplace 命令(不含 egen)等号后面的公式。表达式的暂元形式为 `=expression'= 表明会先计算表达式,再将计算结果赋给暂元。

. dis "`=2+2'"
4

// _N为当前数据集的观测数,输出结果与当前数据集有关
. dis `=_N' 
0

// 暂元中的暂元
. local year 2021
. dis `=`year'-1' 
2020

2.5 存放变量名称

sysuse auto, clear
local varlist price weight rep78 length
// 也可给暂元取其他名字,不叫 varlist
dis "`varlist'" // 列印变量名称
sum `varlist'
des `varlist'

2.6 提取变量标签 (variable label)

提取变量标签的基本语法结构为:

local 暂元名: variable label 变量名称

我们可以结合循环语句提取出所有变量的变量标签,也可以做其他处理。比如下面的小例子展示了如何将一个变量的标签赋给另一个变量。有关循环语句中的暂元使用方法见第 3 节的内容。

sysuse auto,clear 
local lab: var label foreign
// 暂元名称为 lab,保存了 foreign 变量的标签
dis "`lab'" // 列印变量标签
label var mpg "`lab'"
// 将 foreign 变量的标签赋给 mpg 变量

2.7 提取变量值标签 (value label)

提取变量值标签的基本语法结构为:

local 暂元名: value label 变量名称

与提取变量标签类似,只是将 variable label 变成 value label

sysuse auto,clear 
local lab: value label foreign
// 暂元名称为 lab,保存了 foreign 变量的值标签
dis "`lab'" // 列印变量值标签

2.8 提取某一路径下的文件名

提取某一路径下的文件名的基本语法结构为:

local 暂元名: dir "路径名" files "文件筛选规则"

一个简单的例子如下:

// 获取当前路径下的所有 dta 格式文件,
// 并存入暂元 dtas 中
local dtas: dir "." files "*.dta"
dis `"`dtas'"'

实际中,我们可能需要合并某个文件夹下的所有文件(命名规则可能是统一的,也可能是非统一的)。那这时便可以用到这个命令。具体案例见推文 Stata: 如何快速合并 3500 个无规则命名的数据文件?

2.9 小陷阱:未定义的暂元

若使用未定义的暂元或者错误输入暂元的名字,Stata 不会报错。因此当代码比较复杂时,我们一定要谨慎使用暂元,需要检查暂元是否被定义或者暂元名字是否被正确输入。

// 未定义的暂元,返回空值。
display `y'

2.10 在回归中的使用技巧

在写回归命令时,我们可能会输入很多控制变量。这种情况下,暂元的使用会使得命令更简洁,也更具可读性和可重复性。

/*
local controlVars age sex occupation location ///   
                  maritalStatus hasChildren
// 将控制变量都保存在 controlVars 这一暂元里               
reg income education `controlVars' 
logit employed education `controlVars' 
*/                

类似地,当我们需要频繁使用子样本时,我们也可以用暂元定义不同的子样本。

/*
local blackWoman race==1 & female
// 定义了子样本 1,保存在 blackWoman 里
local hispMan race==2 & !female
// 定义了子样本 2,保存在 hispMan 里
reg income education `controlVars' if `blackWoman'
// OLS 回归
logit employed education `controlVars' if `hispMan'
// Logit 回归
*/

当我们需要修改控制变量或子样本的定义时,只需改一次暂元的定义,而不用修改回归命令。在频繁使用回归命令时,这会非常节省时间和减少错误率。

3. for 循环语句中的暂元

Stata 中包含三类循环语句(一般地,编程软件都含有 while 和 for 循环):

  • while 循环
  • foreach 循环
  • forvalue 循环

这些循环语句都包含了局部暂元的使用。循环需要遍历某个对象,对这个对象的每个元素进行同样的操作。这时可以用一个局部暂元存储对象的元素,每循环一次,便更新一次暂元的内容。

在实际编程中,我们主要用到 foreachforvalue。因此本文不讨论 while 循环语句。foreachforvalue 的区别在于:前者的循环对象很丰富,可以包括变量、局部暂元、全局暂元、数值列表、字符列表,而后者的循环对象只能为一连串等间隔的数字。 下文将详细介绍 for 循环语句的使用技巧。

3.1 foreach 的基本语法结构

help foreach

/* 方式一: 使用 in, 后可接任何 list 形式 
foreach lname in any_list{
  commands referring to `lname'
 }
        
*方式二: 使用 of 指定 list 类型,再接 list 
foreach lname of listtype list{
  commands referring to `lname'
}

listtype 表示 list 类型,在 Stata 中有以下类型:
  local 局部暂元列表
  global 全局暂元列表
  varlist 数据集已有的变量列表
  newlist 数据集未有的新变量列表
  numlist 数字型列表

注意:lname 是局部暂元的名称。
*/

在 do file 中编写循环命令时,有三点需要注意:

  1. 完整的循环体以 { 开始,并以 } 结尾。且 { 必须与 foreach 在同一行,} 必须另起一行。

  2. 为了代码的可读性,可以用缩进表示循环的层次。

  3. 列表中的各个元素以一个或多个空格分隔开。

一个简单的例子如下:

在上例中,`color` 为一个局部暂元,用来储存列表的元素。`red blue green` 就是一个列表。

foreach color in red blue green {
  display "`color'"
}

/*输出结果如下:
. foreach color in red blue green {
  2.   display "`color'"
  3. }
red
blue
green

输出时,Stata 自动给循环体编上了行号。
*/

在上例中,color 为一个局部暂元,用来储存列表的元素。red blue green 就是一个列表。

3.2 foreach: 依变量循环

应用场景:回归中,保持解释变量不变,不断更换被解释变量。

sysuse auto, clear
foreach yvar in mpg price displacement {
  reg `yvar' foreign weight
}

基于上例,我们还可以使用 foreach lname of varlist varlist 这种形式的循环语句,如下:

foreach yvar of varlist mpg price displacement {
  reg `yvar' foreign weight
}

这两种方式是等价的。但在某些情况下,第二种方法更优。第一种方法需要手动逐个输入变量的名字,而第二种方式可以用 Stata 针对变量列表的缩写方式,比如 x* 表示以 x 开头的所有变量,x-z 表示从 xz 的所有变量。更多缩写方式可参考 Stata for Researchers 这篇文章。

sysuse auto, clear
rename *, up // 将所有变量的名字变成大写
foreach oldname of varlist * {
  // * 表示所有变量
  local newname=lower("`oldname'")
  // 将所有变量的名字变成小写
  rename `oldname' `newname'
}

3.3 foreach: 依变量名的部分字符循环

应用场景:假设这有一份虚构的宽面板数据 (wide form),记录了 10 个人 12 个月的收入,即有 12 个收入变量:incJan, incFeb, incMar 等。我们试图创建一串 0-1 虚拟变量,表示某个人在这个月是否有收入记录。

clear
use http://www.ssc.wisc.edu/sscc/pubs/files/Stata_prog/months.dta

// 方式一:手动创建 12 个虚拟变量
gen hadIncJan=(incJan>0) if incJan<.
gen hadIncFeb=(incFeb>0) if incFeb<.
*以此类推

// 方式二:foreach
foreach month in Jan Feb Mar Apr May ///
              Jun Jul Aug Sep Oct Nov Dec {
  gen hadInc`month'=(inc`month'>0) if inc`month'<.
}

/*备注:
若你确保数据中没有缺失数据,则可省略 if inc`month'<. 
使用 if inc`month'<. 更严谨,避免因存在缺失值而产生错误结果。
*/

3.4 forvalues 的基本语法结构

help forvalues

/*
forvalues lname = range {
  commands referring to `lname'
}

其中,lname 是局部暂元的名称;
forvalues 可缩写成 forval;
range 表示一连串等间隔的数字序列。
*/

range 的定义方式有以下四种:

  • #1(#d)#2 表示以步长 d 创建等差数列,第一个元素为 #1,最后一个元素为 #2;
  • #1/#2 表示以步长 1 创建等差数列,第一个元素为 #1,最后一个元素为 #2;
  • #1 #t to #2 表示以步长 #t-#1 创建等差数列,第一个元素为#1,最后一个元素为 #2;
  • #1 #t : #2 表示以步长 #t-#1 创建等差数列,第一个元素为#1,最后一个元素为 #2。

比如,1(1)3 表示 1,2,31/3 表示 1,2,31 3 to 6 表示 1,3,51 3:6 表示 1,3,5

3.5 forvalues: 依数字循环

应用场景:假设这有一份虚构的宽面板数据 (wide form),记录了 10 个人 1990-2010 年的收入,即有 21 个工资变量:inc1990, inc1991, inc1992 等。我们试图创建一串 0-1 虚拟变量,表示某个人在这年是否有收入记录。

clear
use http://www.ssc.wisc.edu/sscc/pubs/files/Stata_prog/years.dta
forvalues year=1990/2010 {
  gen hadInc`year'=(inc`year'>0) if inc`year'<.
}

注意:当变量命名包含年份时,用四位年份比两位年份好。比如,用 1990 比 90 好。因为 99 之后为 100,而不是 00。

3.6 foreach + levelsof:依数字循环

应用场景:假设这有一份虚构的调查数据,包含 raceincomeincomeeducation 四个变量。当你想研究年龄和教育程度对收入的影响时,会考虑不同人种的异质性。通常想到的方法是:将样本按人种分为不同的子样本,再分别进行回归,即 by race: regress income age i.education。(i.education 为因子变量,因子变量的使用方法可参考 Stata for Researchers: Working with Groups 这篇文章。)但由于这是份抽样调查数据,不能直接使用回归命令,需要使用 svyset 命令调整权重。

svyset 命令的 4 个要点如下:

  1. svyset psu [pweight=weight] 用来设定数据的抽样方法,其中 psu 表示初级抽样单位 (primary sampling unit),weight 为样本权重。

  2. svy: regress income age i.education 表示调整权重后进行回归。

  3. svy, subpop(if race==1): regress income age i.education 表示调整权重后对 race == 1 的子样本进行回归。

  4. 在用 svy 命令时,不能接 by 命令。

第 4 点导致我们不能用 by 命令同时对多个子样本进行估计。这时,我们可以采用循环语句,如下:

clear
use http://www.ssc.wisc.edu/sscc/pubs/files/Stata_prog/vals.dta
forvalues race=1/3 {
  svy, subpop(if race==`race'): reg income age i.education
}

在当前数据中,人种 race 变量只包含 1,2,3 这三个值。如果需要添加第四个值(表示为其他),我们可能会将其设为 4,将上述循环稍作修改并运行。然而一般调查数据中会将“其他”设为 9。这时不能再使用 forvalues,而应该用 foreach,如下:

clear
use http://www.ssc.wisc.edu/sscc/pubs/files/Stata_prog/vals.dta
foreach race in 1 2 3 9 {
  svy, subpop(if race==`race'): reg income age i.education
}

现实中,如果我们事先并不知道 race 的取值有哪些,或者 race 的取值非常多,那使用上述 foreach 语句便很费时。一方面,我们需要清楚 race 的取值;另一方面,我们需要手动逐个输入。这时,我们可以使用 levelsofrace 变量的不同值保存在一个暂元里,再进行循环,如下:

clear
use http://www.ssc.wisc.edu/sscc/pubs/files/Stata_prog/vals.dta
levelsof race, local(races) 
// races 为局部暂元
dis "`races'"

// 方式一
foreach race in `races' {
  display _newline(2) "Race=`race'"
  *使输出结果更具可读性,
  *添加两行空白行后输出当前 race 的取值。
  svy, subpop(if race==`race'): reg income age i.education
}

// 方式二
foreach race of local races {
  // 注意 local 后的暂元不要加引号
  display _newline(2) "Race=`race'"
  *使输出结果更具可读性,
  *添加两行空白行后输出当前 race 的取值。
  svy, subpop(if race==`race'): reg income age i.education
}

上述例子均为说明循环语句的使用。科研中,若要研究人种的异质性,用交乘项的方法比分样本的方法其实更好,如下:

clear
use http://www.ssc.wisc.edu/sscc/pubs/files/Stata_prog/vals.dta
svy: regress income i.race##(c.age i.education)
// i.xx 和 ## 参考因子变量的使用方法

3.7 巧用不用循环语句的循环命令

Stata 有些命令自带循环功能,可以免去使用循环语句。例子如下:

*例1
// 方式一
sysuse auto, clear
gen y=.
forvalues i=1/`=_N' {
  replace y=mpg[`i'] if _n == `i'
}

// 方式二
sysuse auto, clear
gen y=mpg

// 上述两种方法都将 mpg 的值赋给新变量 y。
*例2
// 方式一
clear
use http://www.ssc.wisc.edu/sscc/pubs/files/Stata_prog/months.dta
foreach month in Jan Feb Mar Apr May Jun ///
                 Jul Aug Sep Oct Nov Dec {
  gen hadInc`month'=(inc`month'>0) if inc`month'<.
}

// 方式二
clear
use http://www.ssc.wisc.edu/sscc/pubs/files/Stata_prog/months.dta
reshape long inc, i(id) j(month) string
gen hadInc=(inc>0) if inc<.
reshape wide inc hadInc, i(id) j(month) string

// 上述两种方法的结果是等价的。

4. 嵌套循环

在一个循环体语句中又包含另一个循环语句,称为循环嵌套,如下:

forval i=1/3 {
  forval j=1/3 {
    display "`i',`j'"
  }
}

/* 输出结果为:
1,1
1,2
1,3
2,1
2,2
2,3
3,1
3,2
3,3
*/

在上述例子中,内层循环为 j,外层循环为 i。对于 i 的每个值,内层循环会运行3次,因此 display 命令会被执行 9 次。

我们考虑一个应用场景:假设这有一份虚构的宽面板数据 (wide form),记录了 10 个人 1990-2010 年每个月的收入,即有 21*12=252 个收入变量:incJan1990, incFeb1990等。我们试图创建一串 0-1 虚拟变量,表示某个人在这个月是否有收入记录。

clear
use http://www.ssc.wisc.edu/sscc/pubs/files/Stata_prog/monthyear.dta
forval year=1990/2010 {
  foreach month in Jan Feb Mar Apr May Jun ///
                   Jul Aug Sep Oct Nov Dec {
    gen hadInc`month'`year'=(inc`month'`year'>0) ///
	    if inc`month'`year'<.
  }
}

// 在上面的基础上,在变量名字中的年月信息变为数字,
// 比如,hadIncJan1990 变为 hadInc1。
// 这样使得时间为连续的数字,以数字表示先后顺序。
// 这样就不需要判断1991年1月的前一个月是1990年12月。
local period 1
forval year=1990/2010 {
  foreach month in Jan Feb Mar Apr May Jun ///
                   Jul Aug Sep Oct Nov Dec {
    rename inc`month'`year' inc`period'
    rename hadInc`month'`year' hadInc`period'
    local period=`period'+1
  }
}

5. 总结

暂元的使用既能提高工作效率,又能提高代码的可读性。暂元常和循环语句结合使用。

在用循环语句时,我们最好先厘清自己的思路。比如,我们先分析问题,判断是否需要重复执行某个或某些命令;若需要,则考虑循环语句。在写循环语句时,可先以列表中的单个元素为对象,写好命令;最好再将该元素换成暂元。

6. 参考资料

7. 相关推文

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