该课程已经讲解完,感兴趣的小伙伴可以点击文末的阅读原文跳转到 RStata 平台观看视频讲解。
最近有个小伙伴问了这样的一个问题:
求助:如果我想删除一个变量中重复出现的字符应该如何操作?例如:"1;2;7;1;2;3;4;6;7;1;2;3;4;5;6;7"转化为"1;2;3;4;5;6;7" "1;2;6;7;1;2;4;6"转化为"1;2;4;6;7" 用Stata如何实现?
结果中的数字顺序可以不用从小到大排列。
比较传统的方法是先把变量使用 split 分割开,转换成长数据之后再处理:
clear all
input str30 v
"1;2;7;1;2;3;4;6;7;1;2;3;4;5;6;7"
"1;2;6;7;1;2;4;6"
end
gen id = _n
split v, parse(";")
gather v1-v15
drop if mi(value)
*- 去除重复的
duplicates drop id value, force
spread var value
ren v oldvar
unite v*, sep(;) gen(newvar)
drop v*
replace newvar = subinstr(newvar, ";;", ";", .)
replace newvar = subinstr(newvar, ";;", ";", .)
replace newvar = subinstr(newvar, ";;", ";", .)
list
*> +------------------------------------------------------+
*> | oldvar id newvar |
*> |------------------------------------------------------|
*> 1. | 1;2;6;7;1;2;4;6 2 1;2;6;7;4 |
*> 2. | 1;2;7;1;2;3;4;6;7;1;2;3;4;5;6; 1 1;2;3;4;5;6;7; |
*> +------------------------------------------------------+
不过当数据量很大的时候,split 和 gahter 的速度会非常慢,这种方法就不是什么好方法了。
因此我们可以考虑使用 Mata 来提升速度。
例如对于字符串“1;2;7;1;2;3;4;6;7;1;2;3;4;5;6;”:
mata:
a = ustrsplit("1;2;7;1;2;3;4;6;7;1;2;3;4;5;6;7", ";")
a
n = cols(a)
n
b = a[1]
for(i=2;i<=n;i++) {
if(indexnot(a[i], invtokens(b, ";"))) {
b = (b, a[i])
}
}
invtokens(b, ";")
end
ustrsplit() 函数的功能是把字符串根据“;”拆分成字符串向量(行向量);cols() 函数的功能是计算行向量的长度(列数);然后生成一个 b 向量来存储结果,循环判断 a 向量里面的每个元素是否在 b 中,如果不在 b 中就把该元素放到 b 中。invtokens(b, ";") 是把向量再合并成字符串,indexnot() 表示判断一个字符串是否在另外一个字符串里面。
如果想要把 mata 处理的结果存储到 local 变量里面可以编写如下函数:
mata:
mata clear
void uniquefun(string x) {
a = ustrsplit(x, ";")
b = a[1]
for(i=2;i<=cols(a);i++) {
if(indexnot(a[i], invtokens(b, ";"))) {
b = (b, a[i])
}
}
st_local("uniquefunres", invtokens(b, ";"))
}
end
mata: uniquefun("1;2;7;1;2;3;4;6;7;1;2;3;4;5;6;7")
di `"`uniquefunres'"'
*> 1;2;7;3;4;6;5
如果想要处理整个变量,可以再加一个循环:
clear all
mata:
mata clear
void uniquefun(string x) {
v = st_sdata(., x)
uniquefunres = J(rows(v), 1, "")
for(z=1;z<=rows(v);z++){
a = ustrsplit(v[z], ";")
b = a[1]
for(i=2;i<=cols(a);i++) {
if(indexnot(a[i], invtokens(b, ";"))) {
b = (b, a[i])
}
}
uniquefunres[z] = invtokens(b, ";")
}
st_addvar("strL", "newufvar")
st_sstore(., "newufvar", uniquefunres)
}
end
clear
input str30 v
"1;2;7;1;2;3;4;6;7;1;2;3;4;5;6;7"
"1;2;6;7;1;2;4;6"
end
mata: uniquefun("v")
list
*> +------------------------------------------------+
*> | v newufvar |
*> |------------------------------------------------|
*> 1. | 1;2;7;1;2;3;4;6;7;1;2;3;4;5;6; 1;2;7;3;4;6;5 |
*> 2. | 1;2;6;7;1;2;4;6 1;2;6;7;4 |
*> +------------------------------------------------+
这里的外层循环会循环每个观测值,然后处理结果再存储到 uniquefunres 中,最后再根据 uniquefunres 创建新的变量 newufvar 就是我们想要的结果了。
可以看到这种操作其实非常适合编写一个 egen 函数来实现,把下面的代码保存为 _gipc_unique.ado
文件:
*! 微信公众号 RStata
*! 用于将字符串使用特定字符拆分后,提取所有互不相同的结果
*! 使用示例:egen sub2ipc_unique = ipc_unique(sub2ipc), parse(;)
program define _gipc_unique
version 6.0
gettoken type 0 : 0
gettoken g 0 : 0
gettoken eqs 0 : 0
syntax varlist(max=1 string), parse(string)
if "`parse'" == "" {
local parse = ";"
}
qui mata: ipc_unique("`varlist'", "`parse'")
rename newufvar `g'
end
mata:
void ipc_unique(string x, string parse) {
v = st_sdata(., x)
uniquefunres = J(rows(v), 1, "")
for(z=1;z<=rows(v);z++){
a = ustrsplit(v[z], parse)
b = uniqrows(rowshape(a, cols(a)))
uniquefunres[z] = invtokens(colshape(b, rows(b)), parse)
}
st_addvar("strL", "newufvar")
st_sstore(., "newufvar", uniquefunres)
}
end
这个代码里面我使用了 uniqrows() 函数,该函数的功能是对向量进行排序和去重。前面的代码其实也可以直接用这个函数。
然后就可以使用了:
clear all
input str30 v
"1;2;7;1;2;3;4;6;7;1;2;3;4;5;6;7"
"1;2;6;7;1;2;4;6"
end
egen v2 = ipc_unique(v), parse(;)
list
*> +-------------------------------------------------+
*> | v v2 |
*> |-------------------------------------------------|
*> 1. | 1;2;7;1;2;3;4;6;7;1;2;3;4;5;6; ;1;2;3;4;5;6;7 |
*> 2. | 1;2;6;7;1;2;4;6 1;2;4;6;7 |
*> +-------------------------------------------------+
再回到专利数据上。实际上这个数据的示例也正是专利数据的 IPC 分类号处理问题。
附件中的 data.dta 是一份专利的样本:
clear all
use data, clear
ren 分类号 IPC
删除设计专利:
*- 删除设计专利
drop if !index(IPC, "/")
经常我们需要分析 IPC 分类号中的大类,类似上面的代码,我们可以编写一个 egen 函数用于提取专利 IPC 分类号的前三位(大类):_gipc_unique_sub.ado
,核心的 Mata 代码是:
mata:
string vector uniquerow(string vector x) {
b = uniqrows(rowshape(x, cols(x)))
return(colshape(b, rows(b)))
}
void ipc_unique_sub(string x, string parse, numeric from, numeric to) {
v = st_sdata(., x)
ipccountres = J(rows(v), 1, "")
for(z=1;z<=rows(v);z++){
a = ustrsplit(v[z], parse)
b = substr(a, from, to)
ipccountres[z] = invtokens(uniquerow(b), parse)
}
stata("cap drop newufvar")
st_addvar("strL", "newufvar")
st_sstore(., "newufvar", ipccountres)
}
end
uniquerow() 函数的功能是去除行向量中的重复值;substr() 的功能自然就是提取子字符串了。
使用:
*- 获取所有的大类(去除重复的):_gipc_unique_sub.ado
egen subipc_unique = ipc_unique_sub(IPC), parse(;) from(1) to(3)
在专利 IPC 分类号中我们还经常会遇到需要统计分类号数量的问题。一个传统的方法是可以使用 moss 命令:
moss IPC, match(";")
replace _count = _count + 1
ren _count n
drop _*
实际上这个也可以用 mata 来实现:
mata:
a = ustrsplit("C02F9/08(2006.01)I;B01D36/04(2006.01)I;C02F1/52(2006.01)I;C02F1/38(2006.01)I;C02F1/467(2006.01)I", ";")
cols(a)
end
*> 5
据此,我们也可以编写一个 egen 函数:_gipc_count.ado
使用:
egen n2 = ipc_count(IPC), parse(;)
更多的时候我们想要统计的是互不相同的分类号数量:_gipc_unique_count.ado
egen n3 = ipc_unique_count(IPC), parse(;)
该函数的核心 Mata 代码是:
mata:
string vector uniquerow(string vector x) {
b = uniqrows(rowshape(x, cols(x)))
return(colshape(b, rows(b)))
}
void ipc_unique_count(string x, string parse) {
v = st_sdata(., x)
ipccountres = J(rows(v), 1, .)
for(z=1;z<=rows(v);z++){
a = ustrsplit(v[z], parse)
ipccountres[z] = cols(uniquerow(a))
}
stata("cap drop newufvar")
st_addvar("double", "newufvar")
st_store(., "newufvar", ipccountres)
}
end
还有一些时候我们想要统计的是专利大类的数量(IPC 分类号的前三位),虽然这个可以在前面提取的基础上直接计数,但是我担心组合使用 egen 函数会影响效率,所以还是编写了一个函数:_gipc_sub_count.ado
*- 但是这里需要注意要提前把一些特别的符号去除:
replace IPC = subinstr(IPC, "//", "", .)
replace IPC = subinstr(IPC, "/", "", .)
replace IPC = subinstr(IPC, "(", "", .)
replace IPC = subinstr(IPC, ")", "", .)
egen n4 = ipc_sub_count(IPC), parse(;) from(1) to(3)
*- 或者在前面计算的 subipc_unique 的基础上:
egen n4_2 = ipc_count(subipc_unique)
有些论文中会选择 IPC 的前四位(小类):
*- 提取 4 位
egen subipc_unique14 = ipc_unique_sub(IPC), parse(;) from(1) to(4)
如果不想去除重复,可以使用:_gipc_sub.ado
:
egen subipc13 = ipc_sub(IPC), parse(;) from(1) to(3)
egen subipc14 = ipc_sub(IPC), parse(;) from(1) to(4)
有时候需要提取“/”前面的部分(大组):_gipc_sub2.ado
clear all
use data, clear
ren 分类号 IPC
drop if !index(IPC, "/")
replace IPC = subinstr(IPC, "//", "", .)
replace IPC = subinstr(IPC, "(", "", .)
replace IPC = subinstr(IPC, ")", "", .)
egen sub2ipc = ipc_sub2(IPC), parse(;) parse2(/) choose(1)
提取后面的选择 2 就好(这个通常没什么用):
egen sub2ipc2 = ipc_sub2(IPC), parse(;) parse2(/) choose(2)
该函数的核心 Mata 代码为:
mata:
void ipc_sub2(string x, string parse, string parse2, numeric choose) {
v = st_sdata(., x)
ipccountres = J(rows(v), 1, "")
for(z=1;z<=rows(v);z++){
a = ustrsplit(v[z], parse)
ipccountres2 = J(1, cols(a), "")
for(t=1;t<=cols(a);t++){
b = ustrsplit(a[t], parse2)
if (choose <= cols(b)) {
ipccountres2[t] = b[choose]
}
}
ipccountres[z] = invtokens(ipccountres2, parse)
}
stata("cap drop newufvar")
st_addvar("strL", "newufvar")
st_sstore(., "newufvar", ipccountres)
}
end
也就是在使用 parse 分割之后又使用 parse2 分割了一次。
去除 sub2ipc 中重复的 IPC 分类号可以使用:_gipc_unique.ado
:
egen sub2ipc_unique = ipc_unique(sub2ipc), parse(;)
重复使用多个 egen 函数,可能会降低效率,所以还是单独编写一个函数:_gipc_sub2_unique.ado
egen sub2ipc3 = ipc_sub2_unique(IPC), parse(;) parse2(/) choose(1)
这些基本就是专利 IPC 分类号处理中常用的一些操作了~
最后再汇总下这几个函数的功能:
_gipc_unique.ado:用于将字符串使用特定字符拆分后,提取所有互不相同的结果 _gipc_count.ado:用于统计字符串变量使用特定分隔符切分得到的子字符串数量 _gipc_sub_count.ado:用于统计字符串使用特定字符拆分后,子字符串的子字符串互不相同的数量,例如前 3 位的 _gipc_sub.ado:用于提取字符串使用特定字符拆分后得到的子字符串特定范围的子字符串 _gipc_sub0.ado:用于统计字符串使用特定字符拆分后,第 n 个子字符串 _gipc_sub2_unique.ado:用于将字符串使用特定字符拆分后,再对子字符串使用另一字符拆分,进而提取拆分结果的第 n 部分(去除重复的) _gipc_sub2.ado:用于将字符串使用特定字符拆分后,再对子字符串使用另一字符拆分,进而提取拆分结果的第 n 部分 _gipc_unique_count.ado:用于将字符串使用特定字符拆分后,统计互不相同的子字符串数量 _gipc_unique_sub.ado:用于提取字符串使用特定字符拆分后得到的子字符串特定范围的子字符串(去除重复的)
非常绕~ 大家使用的时候好好研究下,注意别用错了。
直播信息
该课程已经讲解完,感兴趣的小伙伴可以点击文末的阅读原文跳转到 RStata 平台观看视频讲解。
直播地址:腾讯会议(需要报名 RStata 培训班参加) 讲义材料:需要购买 RStata 名师讲堂会员,详情可阅读:一起来学习 R 语言和 Stata 啦!学习过程中遇到的问题也可以随时提问!
更多关于 RStata 会员的更多信息可添加微信号 r_stata 咨询:
附件下载(点击文末的阅读原文即可跳转):
https://rstata.duanshu.com/#/brief/course/1cfb2e8e8e5f4716959fc78bbaf6d446