Stata中如何实现“字典”功能?

Stata语言在设计上完全以统计功能为核心,它并不以简洁的语法见长。不过,我们仍然可以用稍微复杂一点的代码,实现它不那么擅长的功能。

例如,我们想要进行一些一一对应的批量操作时,可以在Python等语言中构建字典(dictionary)变量。

通过循环语句计数器暂元的结合使用,我们可以在Stata上实现类似功能。

一、部分语句提供类似“字典”的语法

其实,有些语句的语法提供了类似的“一一对应”功能。例如,我们想要对变量进行批量重命名时,可以逐个重命名:

sysuse nlsw88, clear
rename age 年龄
rename race 种族
rename married 婚姻状况
rename occupation 职业

也可以使用以下更简洁的代码

sysuse nlsw88, clear
rename (age race married occupation) (年龄 种族 婚姻状况 职业)

或者定义两个局部暂元(local macro),使得代码的逻辑更清晰:

sysuse nlsw88, clear
local old_varnames age race married occupation
local new_varnames 年龄 种族 婚姻状况 职业
rename (`old_varnames') (`new_varnames')

当需要重命名的变量很多时,后两种写法有明显优势。

三种写法的运行结果是一致的:

image-20220316094832045

二、手动构建“字典”

但有些语句并不像rename语句一样支持这样的“一一对应”功能。例如,执行完上述代码之后,我们在Data Editor (Edit) 界面右击,选择“Data - Value labels - Manage value labels…”,发现值标签(value labels)的名字依然是旧的:

image-20220316095133558

这个时候,我们希望能把值标签的名字也同步更新(这在大型数据集中非常重要)。这需要使用label copy语句先复制一份原来的标签出来,然后再给新变量贴上新的值标签:

sysuse nlsw88, clear
label copy racelbl 种族值标签
label copy marlbl 婚姻状况值标签
label copy occlbl 职业值标签
rename age 年龄
rename race 种族
rename married 婚姻状况
rename occupation 职业
label var 种族 种族值标签
label var 婚姻状况 婚姻状况值标签
label var 职业 职业值标签

这样就可以更方便地找到新变量名对应的值标签,当然,新的值标签和旧变量名对应的值标签是完全一致的:

image-20220316095937568

然而,label copy语句并不支持label copy (racelbl marlbl occlbl) (种族值标签 婚姻状况值标签 职业值标签)这样的“一一对应”语法功能,只能一行行写重复的代码。当我们需要操作的值标签很多时,这就很耗时耗力。

一种常见的解决办法是使用其他工具(如Excel、Python或一些高级的代码编辑器)批量生成结构相同的Stata语句,再复制到do文件中。这种方法虽然可行,但并不优雅,而且会使后期对代码的勘误更加困难。

这时,我们可以结合循环语句计数器暂元,完成想要的功能:

sysuse nlsw88, clear
local old_varnames age race married occupation
local new_varnames 年龄 种族 婚姻状况 职业
local new_vars_with_val_labels 种族 婚姻状况 职业
local old_value_labels racelbl marlbl occlbl
local new_value_labels 种族值标签 婚姻状况值标签 职业值标签

// 复制变量值标签
local i = 0
foreach old in `old_value_labels' {
    local j = 0
    foreach new in `new_value_labels' {
        if `i' == `j' {
            label copy `old' `new'
        }
        local j = `j' + 1
    }
    local i = `i' + 1
}

// 重命名变量
rename (`old_varname') (`new_varname')

// 给重命名后的变量贴上新的值标签
local i = 0
foreach var of varlist `new_vars_with_val_labels' {
    local j = 0
    foreach val_label in `new_value_labels' {
        if `i' == `j' {
            label var `var' `val_label'
        }
        local j = `j' + 1
    }
    local i = `i' + 1
}

虽然在示例代码中,第二种写法比第一种还多了二十几行,但是第一种写法的代码行数和涉及的变量数量成正比,第二种写法的代码行数却不随变量数的增加而增加(除了在定义暂元时,为了避免单行过长而手动折行,可能会增加若干行)。当我们需要批量转换的变量有几十个、上百个乃至更多时,后一种的优势自然就体现出来了。

可以看到,这段代码实现了我们想要的功能:

image-20220316102923884

这里我们给值标签的起名字,都是变量名后面加上“值标签”三个字,所以我们还可以进一步精简上面的代码:

sysuse nlsw88, clear
local old_varnames age race married occupation
local new_varnames 年龄 种族 婚姻状况 职业
local new_vars_with_val_labels 种族 婚姻状况 职业
local old_value_labels racelbl marlbl occlbl

local i = 0
foreach old in `old_value_labels' {
    local j = 0
    foreach new in `new_vars_with_val_labels' {
        if `i' == `j' {
            label copy `old' `new'值标签
        }
        local j = `j' + 1
    }
    local i = `i' + 1
}

rename (`old_varname') (`new_varname')

foreach var of varlist `new_vars_with_val_labels' {
    label var `var' `var'值标签
}

三、简析

这里能实现类似“字典”功能,主要依靠两个循环结构的嵌套,以及两个计数器暂元ij

首先,外层循环遍历旧的值标签,每执行完一次循环体,计数器暂元i就增加1。换言之,i代表外层循环的循环体运行到了第几次

local i = 0
foreach old in `old_value_labels' {
    local j = 0
    foreach new in `new_value_labels' {
        if `i' == `j' {
            label copy `old' `new'
        }
        local j = `j' + 1
    }
    local i = `i' + 1
}

然后,内层循环遍历旧的值标签,每执行完一次循环体,计数器暂元j就增加1。同理,j代表内层循环的循环体运行到了第几次

local i = 0
foreach old in `old_value_labels' {
    local j = 0
    foreach new in `new_value_labels' {
        ...
        local j = `j' + 1
    }
    local i = `i' + 1
}

最后,为了完成“一一对应”,我们仅在ij相等时进行复制值标签操作,即当且仅当:

  • 外层循环遍历至第1个旧的值标签,同时内层循环遍历至第1个新的值标签;或
  • 外层循环遍历至第2个旧的值标签,同时内层循环遍历至第2个新的值标签;或
  • ……
  • 外层循环遍历至第n个旧的值标签,同时内层循环遍历至第n个新的值标签

时,我们进行复制值标签操作:

local i = 0
foreach old in `old_value_labels' {
    local j = 0
    foreach new in `new_value_labels' {
        if `i' == `j' {
            label copy `old' `new'
        }
        local j = `j' + 1
    }
    local i = `i' + 1
}

简而言之,当我们有两组内容,并且希望按顺序将这两组数据中对应的内容一一配对进行操作时,就可以使用上面的代码语法实现。这和Python等语言中“字典”变量的功能是类似的。