13.2 清理字符串

早在第7章,我们了解到一些简单的字符串操作任务,如使用paste把字符串合并在一起,以及使用substring提取部分字符串等。

一个非常普遍的问题是:逻辑值何时会被编码成R不理解的值。在alpe_d_huez循环数据集中,DrugUse列(指出每个车手是否曾被指控服用违禁药)中数值被编码为“Y”和“N",而不是TRUE或FALSE。对于这种简单的匹配关系,我们可以直接使用正确的逻辑值替换掉每个字符串:

  1. yn_to_logical <- function(x)
  2. {
  3. y <- rep.int(NA, length(x))
  4. y[x == "Y"] <- TRUE
  5. y[x == "N"] <- FALSE
  6. y
  7. }

默认把值设为NA可以让我们处理那些不能匹配“Y”或“N”的字符串。我们可以显式地调用函数:

  1. alpe_d_huez$DrugUse <- yn_to_logical(alpe_d_huez$DrugUse)

这种直接将一个字符串替换为另一个的方法,对于那些需要替换许多字符串的来说扩展性不是很好。如果你有一万个可能的输入,那么使用一个函数来逐个替换会很难避免书写出错,这种代码也很难维护。

幸运的是,有其他更巧妙的方法可以相对容易地检测、提取和替换掉与模式相匹配的部分字符串。R有一系列(大致上)基于Unix grep工具的内置函数能处理这些任务。它们接受一个要操作的字符串以及要匹配的正则表达式。正如在第1章所说的,正则表达式是一种模式,它能非常灵活地描述字符串的内容。在匹配诸如电话号码或电子邮件地址这种复杂的字符串数据类型时,它们非常有用1。

1参见assertive包为此预装的一些正则表达式。

grepgreplregexpr函数都能找到与模式相匹配的字符串,subgsub函数能替换匹配的字符串。在经典的R风格中,这些函数都是无比准确和非常强大的,但由于历史的原因,它的命名、参数的顺序和返回值都比较奇怪。幸好,就像plyrapply函数,lubridate为日期-时间函数提供了一致的封装一样,stringr包对字符串操作函数也提供了一致的封装。不同之处在于,偶尔需要使用基本的apply函数或日期-时间函数时,stringr已足够先进,你根本无须再使用grep。因此,浏览一下?grep的帮助页面即可,无需投入太多精力。

下例使用learning包中的english_monarchs数据集。它包含了从后罗马时代(5世纪)英格兰被分裂为七王国直到13世纪初英国接管了爱尔兰这段时期的统治者的名字和日期:

  1. data(english_monarchs, package = "learningr")
  2. head(english_monarchs)
  3. ## name house start.of.reign end.of.reign domain
  4. ## 1 Wehha Wuffingas NA 571 East Anglia
  5. ## 2 Wuffa Wuffingas 571 578 East Anglia
  6. ## 3 Tytila Wuffingas 578 616 East Anglia
  7. ## 4 R?dwald Wuffingas 616 627 East Anglia
  8. ## 5 Eorpwald Wuffingas 627 627 East Anglia
  9. ## 6 Ricberht Wuffingas 627 630 East Anglia
  10. ## length.of.reign.years reign.was.more.than.30.years
  11. ## 1 NA NA
  12. ## 2 7 FALSE
  13. ## 3 38 TRUE
  14. ## 4 11 FALSE
  15. ## 5 0 FALSE
  16. ## 6 3 FALSE

历史的问题之一是它的数据实在太多了。幸好,古怪或杂乱的数据会将你引向历史中有意思的那部分,即我们可以压缩数据而聚焦到有趣的部分。例如,尽管英格兰地区有七个王国,但它们的边界却极不确定,有时一个王国会征服另一个。我们可以通过在domain列中搜索逗号找到这些交叉点。为了检测出相关的模式,我们使用str_detect函数。fixed函数告诉str_detect:我们正在寻找一个固定字符串(逗号)而非正则表达式。str_detect将返回一个可用于索引的逻辑向量:

  1. library(stringr)
  2. multiple_kingdoms <- str_detect(english_monarchs$domain, fixed(","))
  3. english_monarchs[multiple_kingdoms, c("name", "domain")]
  4. ## name domain
  5. ## 17 Offa East Anglia, Mercia
  6. ## 18 Offa East Anglia, Kent, Mercia
  7. ## 19 Offa and Ecgfrith East Anglia, Kent, Mercia
  8. ## 20 Ecgfrith East Anglia, Kent, Mercia
  9. ## 22 Cҩnwulf East Anglia, Kent, Mercia
  10. ## 23 Cҩnwulf and Cynehelm East Anglia, Kent, Mercia
  11. ## 24 Cҩnwulf East Anglia, Kent, Mercia
  12. ## 25 Ceolwulf East Anglia, Kent, Mercia
  13. ## 26 Beornwulf East Anglia, Mercia
  14. ## 82 Ecgbehrt and Æthelwulf Kent, Wessex
  15. ## 83 Ecgbehrt and Æthelwulf Kent, Mercia, Wessex
  16. ## 84 Ecgbehrt and Æthelwulf Kent, Wessex
  17. ## 85 Æthelwulf and Æðelstan I Kent, Wessex
  18. ## 86 Æthelwulf Kent, Wessex
  19. ## 87 Æthelwulf and Æðelberht III Kent, Wessex
  20. ## 88 Æðelberht III Kent, Wessex
  21. ## 89 Æthelred I Kent, Wessex
  22. ## 95 Oswiu Mercia, Northumbria

同样常见的是,王国的权力并非为某位统治者专有,而是由几个人分享(当一个强大的国王有好几个儿子时特别常见)。我们可以通过在name一栏中寻找逗号或“and”来发现这些例子。这一次,因为要同时寻找两件事情,所以更简单的做法是使用一个正则表达式而非固定的字符串。在正则表达式和R中,管道字符|的含义均为:或。

在下例中,为防止输出内容太多,我们只返回name一列,且忽略缺失值(使用is.na):

  1. multiple_rulers <- str_detect(english_monarchs$name, ",|and")
  2. english_monarchs$name[multiple_rulers & !is.na(multiple_rulers)]
  3. ## [1] Sigeberht and Ecgric
  4. ## [2] Hun, Beonna and Alberht
  5. ## [3] Offa and Ecgfrith
  6. ## [4] Cҩnwulf and Cynehelm
  7. ## [5] Sighere and Sebbi
  8. ## [6] Sigeheard and Swaefred
  9. ## [7] Eorcenberht and Eormenred
  10. ## [8] Oswine, Swæfbehrt, Swæfheard
  11. ## [9] Swæfbehrt, Swæfheard, Wihtred
  12. ## [10] Æðelberht II, Ælfric and Eadberht I
  13. ## [11] Æðelberht II and Eardwulf
  14. ## [12] Eadberht II, Eanmund and Sigered
  15. ## [13] Heaberht and Ecgbehrt II
  16. ## [14] Ecgbehrt and Æthelwulf
  17. ## [15] Ecgbehrt and Æthelwulf
  18. ## [16] Ecgbehrt and Æthelwulf
  19. ## [17] Æthelwulf and Æðelstan I
  20. ## [18] Æthelwulf and Æðelberht III
  21. ## [19] Penda and Eowa
  22. ## [20] Penda and Peada
  23. ## [21] Æthelred, Lord of the Mercians
  24. ## [22] Æthelflæd, Lady of the Mercians
  25. ## [23] Ælfwynn, Second Lady of the Mercians
  26. ## [24] Hálfdan and Eowils
  27. ## [25] Noðhelm and Watt
  28. ## [26] Noðhelm and Bryni
  29. ## [27] Noðhelm and Osric
  30. ## [28] Noðhelm and Æðelstan
  31. ## [29] Ælfwald, Oslac and Osmund
  32. ## [30] Ælfwald, Ealdwulf, Oslac and Osmund
  33. ## [31] Ælfwald, Ealdwulf, Oslac, Osmund and Oswald
  34. ## [32] Cenwalh and Seaxburh
  35. ## 211 Levels: Adda Æðelbehrt Æðelberht I ... Wulfhere

如果想把name一列拆分,使它列出每位统治者的名字,可以使用str_split (或用R基本包中的strsplit,作用基本一样)。str_split接受一个向量作为输入参数,且将返回一个列表,这是因为每个输入的字符串可以被分成长度不同的向量。如果每个输入须返回相同的分割数,则可使用str_split_fixed,它将返回一个矩阵。以下输出显示了多位统治者中的前几个例子:

  1. individual_rulers <- str_split(english_monarchs$name, ", | and ")
  2. head(individual_rulers[sapply(individual_rulers, length) > 1])
  3. ## [[1]]
  4. ## [1] "Sigeberht" "Ecgric"
  5. ##
  6. ## [[2]]
  7. ## [1] "Hun" "Beonna" "Alberht"
  8. ##
  9. ## [[3]]
  10. ## [1] "Offa" "Ecgfrith"
  11. ##
  12. ## [[4]]
  13. ## [1] "Cҩnwulf" "Cynehelm"
  14. ##
  15. ## [[5]]
  16. ## [1] "Sighere" "Sebbi"
  17. ##
  18. ## [[6]]
  19. ## [1] "Sigeheard" "Swaefred"

在此期间,许多盎格鲁 - 撒克逊(Anglo-Saxon)统治者的名字中有古英语字符,像“æ”(“ash”),它代表“ae”或“ð”和“þ”(分别为“eth”和“thorn,”),它们都代表“th.”。在很多情况下,每个统治者名字的拼写并不一致,但要识别某位统治者,其名字拼写就必须固定下来。

让我们来看看有多少个thðþ用来组成字母“th.”。我们可以用str_count计算出它们在每个名称中的出现次数,然后用sum来对所有统治者求和计算出总的出现次数:

  1. th <- c("th", "ð", "þ")
  2. sapply(      # 也可以使用plyr中的laply
  3. th,
  4. function(th)
  5. {
  6. sum(str_count(english_monarchs$name, th))
  7. }
  8. )
  9. ## th ð þ
  10. ## 74 26 7

在这个数据集开始看起来像常见的现代标准拉丁拼写法。如果要替换掉eth和thorn等字符串,可使用str_replace_all。(有一个稍微不同的函数str_replace,它仅仅替换掉第一个匹配的字符串。)把eth和thorn置于在方括号中的意思是:符合下列任一字符:

  1. english_monarchs$new_name <- str_replace_all(english_monarchs$name, "[ðþ]", "th")

这种技巧对于清理一个类别变量的水平值非常有用。例如,性别在英语中可通过多种方式指定,但我们通常只需要其中的两个。在下例中,我们将匹配以“m”开头的(^)、后面跟着一个可选的(?)“ale”、且以字符串($)为结尾:

  1. gender <- c(
  2. "MALE", "Male", "male", "M", "FEMALE",
  3. "Female", "female", "f", NA
  4. )
  5. clean_gender <- str_replace(
  6. gender,
  7. ignore.case("^m(ale)?$"),
  8. "Male"
  9. )
  10. (clean_gender <- str_replace(
  11. clean_gender,
  12. ignore.case("^f(emale)?$"), "
  13. Female"
  14. ))
  15. ## [1] "Male" "Male" "Male" "Male" "Female" "Female" "Female" "Female"
  16. ## [9] NA