写在前面
本系列推文为《R for Data Science (2)》的中文翻译版本。所有内容都通过开源免费的方式上传至Github,欢迎大家参与贡献,详细信息见:
Books-zh-cn 项目介绍:
Books-zh-cn:开源免费的中文书籍社区
r4ds-zh-cn Github 地址:
https://github.com/Books-zh-cn/r4ds-zh-cn
r4ds-zh-cn 网站地址:
https://books-zh-cn.github.io/r4ds-zh-cn/
目录
-
14.4 从字符串中提取数据
-
14.5 字母
-
14.6 非英文文本
-
14.7 总结
14.4.3 诊断不断扩大的问题
separate_wider_delim() 要求固定且已知的列数。 如果某些行没有预期数量的片段会发生什么? 可能存在两种问题:片段过少或过多,因此 separate_wider_delim() 提供了两个参数来帮助处理:too_few 和 too_many。 我们首先通过以下示例数据集看看 too_few 的情况:
df <- tibble(x = c("1-1-1", "1-1-2", "1-3", "1-3-2", "1"))
df |>
separate_wider_delim(
x,
delim = "-",
names = c("x", "y", "z")
)
#> Error in `separate_wider_delim()`:
#> ! Expected 3 pieces in each element of `x`.
#> ! 2 values were too short.
#> ℹ Use `too_few = "debug"` to diagnose the problem.
#> ℹ Use `too_few = "align_start"/"align_end"` to silence this message.
您会注意到出现了错误,但该错误提供了一些后续操作建议。 让我们从调试问题开始:
debug <- df |>
separate_wider_delim(
x,
delim = "-",
names = c("x", "y", "z"),
too_few = "debug"
)
#> Warning: Debug mode activated: adding variables `x_ok`, `x_pieces`, and
#> `x_remainder`.
debug
#> # A tibble: 5 × 6
#> x y z x_ok x_pieces x_remainder
#> <chr> <chr> <chr> <lgl> <int> <chr>
#> 1 1-1-1 1 1 TRUE 3 ""
#> 2 1-1-2 1 2 TRUE 3 ""
#> 3 1-3 3 <NA> FALSE 2 ""
#> 4 1-3-2 3 2 TRUE 3 ""
#> 5 1 <NA> <NA> FALSE 1 ""
使用调试模式时,输出结果会添加三个额外列:x_ok、x_pieces 和 x_remainder(若分离不同名称的变量,前缀会相应变化)。 此处 x_ok 可帮助快速定位失败的输入:
debug |> filter(!x_ok)
#> # A tibble: 2 × 6
#> x y z x_ok x_pieces x_remainder
#> <chr> <chr> <chr> <lgl> <int> <chr>
#> 1 1-3 3 <NA> FALSE 2 ""
#> 2 1 <NA> <NA> FALSE 1 ""
x_pieces 显示找到的片段数量,与预期值 3(即names的长度)相比较。 当片段过少时 x_remainder 没有实际用处,但我们稍后会再次见到它。
有时查看这些调试信息能发现分隔策略的问题,或表明在分离前需要更多预处理。 此时应在上游解决问题,并确保移除 too_few = "debug" 以保证新问题会触发报错。
其他情况下,你可能希望用 NA 填充缺失片段后继续处理。 这时可以使用 too_few = "align_start" 和 too_few = "align_end" 来控制 NA 的填充位置:
df |>
separate_wider_delim(
x,
delim = "-",
names = c("x", "y", "z"),
too_few = "align_start"
)
#> # A tibble: 5 × 3
#> x y z
#> <chr> <chr> <chr>
#> 1 1 1 1
#> 2 1 1 2
#> 3 1 3 <NA>
#> 4 1 3 2
#> 5 1 <NA> <NA>
片段过多时同样适用以下原则:
df <- tibble(x = c("1-1-1", "1-1-2", "1-3-5-6", "1-3-2", "1-3-5-7-9"))
df |>
separate_wider_delim(
x,
delim = "-",
names = c("x", "y", "z")
)
#> Error in `separate_wider_delim()`:
#> ! Expected 3 pieces in each element of `x`.
#> ! 2 values were too long.
#> ℹ Use `too_many = "debug"` to diagnose the problem.
#> ℹ Use `too_many = "drop"/"merge"` to silence this message.
但现在,当我们调试结果时,可以看到 x_remainder 的作用:
debug <- df |>
separate_wider_delim(
x,
delim = "-",
names = c("x", "y", "z"),
too_many = "debug"
)
#> Warning: Debug mode activated: adding variables `x_ok`, `x_pieces`, and
#> `x_remainder`.
debug |> filter(!x_ok)
#> # A tibble: 2 × 6
#> x y z x_ok x_pieces x_remainder
#> <chr> <chr> <chr> <lgl> <int> <chr>
#> 1 1-3-5-6 3 5 FALSE 4 -6
#> 2 1-3-5-7-9 3 5 FALSE 5 -7-9
处理过多片段时选项略有不同:可以静默"丢弃"额外片段,或将其全部"合并"到最后一列:
df |>
separate_wider_delim(
x,
delim = "-",
names = c("x", "y", "z"),
too_many = "drop"
)
#> # A tibble: 5 × 3
#> x y z
#> <chr> <chr> <chr>
#> 1 1 1 1
#> 2 1 1 2
#> 3 1 3 5
#> 4 1 3 2
#> 5 1 3 5
df |>
separate_wider_delim(
x,
delim = "-",
names = c("x", "y", "z"),
too_many = "merge"
)
#> # A tibble: 5 × 3
#> x y z
#> <chr> <chr> <chr>
#> 1 1 1 1
#> 2 1 1 2
#> 3 1 3 5-6
#> 4 1 3 2
#> 5 1 3 5-7-9
14.5 字母
本节我们将介绍处理字符串内单个字母的函数。 您将学习如何获取字符串长度、提取子字符串,以及在图表和表格中处理长字符串的方法。
14.5.1 长度
str_length() 可显示字符串包含的字母数量:
str_length(c("a", "R for data science", NA))
#> [1] 1 18 NA
您可以将其与 count() 结合使用来统计美国婴儿名字的长度分布,再通过 filter() 查看最长的名字,目前最长名字有 15 个字母:
babynames |>
count(length = str_length(name), wt = n)
#> # A tibble: 14 × 2
#> length n
#> <int> <int>
#> 1 2 338150
#> 2 3 8589596
#> 3 4 48506739
#> 4 5 87011607
#> 5 6 90749404
#> 6 7 72120767
#> # ℹ 8 more rows
babynames |>
filter(str_length(name) == 15) |>
count(name, wt = n, sort = TRUE)
#> # A tibble: 34 × 2
#> name n
#> <chr> <int>
#> 1 Franciscojavier 123
#> 2 Christopherjohn 118
#> 3 Johnchristopher 118
#> 4 Christopherjame 108
#> 5 Christophermich 52
#> 6 Ryanchristopher 45
#> # ℹ 28 more rows
14.5.2 子集化
您可以使用 str_sub(string, start, end) 来提取字符串的部分内容,其中 start 和 end 位置指定了子串的开始和结束点。start 和 end 参数具有包含性,因此返回字符串的长度将为 end - start + 1:
x <- c("Apple", "Banana", "Pear")
str_sub(x, 1, 3)
#> [1] "App" "Ban" "Pea"
您可以使用负数值从字符串末尾向前计数:-1 表示最后一个字符,-2 表示倒数第二个字符,依此类推:
str_sub(x, -3, -1)
#> [1] "ple" "ana" "ear"
请注意,如果字符串过短,str_sub() 不会报错:它会尽可能返回可用内容:
str_sub("a", 1, 5)
#> [1] "a"
我们可以结合 str_sub() 与 mutate() 来找出每个名字的首字母和尾字母:
babynames |>
mutate(
first = str_sub(name, 1, 1),
last = str_sub(name, -1, -1)
)
#> # A tibble: 1,924,665 × 7
#> year sex name n prop first last
#> <dbl> <chr> <chr> <int> <dbl> <chr> <chr>
#> 1 1880 F Mary 7065 0.0724 M y
#> 2 1880 F Anna 2604 0.0267 A a
#> 3 1880 F Emma 2003 0.0205 E a
#> 4 1880 F Elizabeth 1939 0.0199 E h
#> 5 1880 F Minnie 1746 0.0179 M e
#> 6 1880 F Margaret 1578 0.0162 M t
#> # ℹ 1,924,659 more rows
14.5.3 练习
-
在计算婴儿名字长度分布时,我们为何使用
wt = n参数? -
运用
str_length()和str_sub()函数提取每个婴儿名字的中间字母。如果字符串包含偶数个字符,您将如何处理? -
婴儿名字的长度随时间推移是否存在显著趋势?首字母和尾字母的流行度又有哪些变化?
14.6 非英文文本
迄今为止,我们主要关注英语文本的处理,这类文本操作起来特别方便,原因有二。 首先,英文字母表相对简单,仅包含 26 个字母。 其次(或许更重要的),当今使用的计算基础设施主要由英语使用者设计。 遗憾的是,我们无法全面探讨非英语语言的处理,但仍希望提醒您注意可能遇到的几个主要挑战:字符编码、字母变体以及依赖区域设置的函数。
14.6.1 编码
处理非英语文本时,第一个挑战通常是编码(encoding) 问题。 要理解其中的原理,我们需要深入探究计算机如何表示字符串。 在 R 中,可以使用 charToRaw() 获取字符串的底层表示:
charToRaw("Hadley")
#> [1] 48 61 64 6c 65 79
这六个十六进制数字分别代表一个字母:48是H,61是a,依此类推。 从十六进制数字到字符的映射称为编码,这里的编码叫做 ASCII。 ASCII 能出色地表示英文字符,因为它是美国信息交换标准代码。
但对非英语语言来说情况就不那么简单了。 在计算机早期阶段,存在许多相互竞争的非英语字符编码标准。 例如欧洲曾有两种不同编码:Latin1(即ISO-8859-1)用于西欧语言,而Latin2(即ISO-8859-2)用于中欧语言。 在Latin1中,字节b1是"±",但在Latin2中却是"ą"! 幸运的是,如今有一个几乎无处不在的标准:UTF-8。 UTF-8 可以编码当今人类使用的几乎所有字符,以及许多额外符号(如表情符号)。
readr 在所有地方都使用 UTF-8。 这是个很好的默认设置,但对于不使用 UTF-8 的旧系统产生的数据会读取失败。 发生这种情况时,打印字符串会显示异常。 有时可能只是一两个字符乱码,有时则会得到完全无法识别的乱码。 例如以下是两个采用非常见编码的内联CSV文件:
x1 <- "text\nEl Ni\xf1o was particularly bad this year"
read_csv(x1)$text
#> [1] "El Ni\xf1o was particularly bad this year"
x2 <- "text\n\x82\xb1\x82\xf1\x82\xc9\x82\xbf\x82\xcd"
read_csv(x2)$text
#> [1] "\x82\xb1\x82\xf1\x82ɂ\xbf\x82\xcd"
要正确读取这些数据,需要通过locale参数指定编码:
read_csv(x1, locale = locale(encoding = "Latin1"))$text
#> [1] "El Niño was particularly bad this year"
read_csv(x2, locale = locale(encoding = "Shift-JIS"))$text
#> [1] "こんにちは"
如何找到正确的编码? 如果幸运的话,数据文档的某个地方会注明编码方式。 但遗憾的是这种情况很少见,因此 readr 提供 guess_encoding() 来帮助您识别。 虽然这种方法并非万无一失,且文本量越大效果越好(与当前示例不同),但作为起点是合理的。 通常需要尝试多种编码才能找到正确的方案。
编码是一个丰富而复杂的主题;我们在此仅触及表面。 若想深入了解,建议阅读 http://kunststube.net/encoding/.
14.6.2 字母变化
处理带重音符号的语言时,确定字母位置(例如使用 str_length() 和 str_sub())会面临重大挑战,因为带重音字母可能被编码为单个字符(如ü),也可能通过组合无重音字母(如u)和变音符号(如¨)形成两个字符。 例如以下代码展示了两种看起来完全相同的ü表示方式:
u <- c("\u00fc", "u\u0308")
str_view(u)
#> [1] │ ü
#> [2] │ ü
但两个字符串的长度不同,且它们的首字符也不同:
str_length(u)
#> [1] 1 2
str_sub(u, 1, 1)
#> [1] "ü" "u"
最后要注意的是:使用 == 比较这些字符串时会被解析为不同,而 stringr 中的实用函数 str_equal() 能识别它们具有相同显示效果:
u[[1]] == u[[2]]
#> [1] FALSE
str_equal(u[[1]], u[[2]])
#> [1] TRUE
14.6.3 与语言环境相关的函数
最后要注意的是:有部分stringr函数的行为会依赖于区域(locale) 设置。 区域设置类似于语言选项,但包含可选的地区标识符以处理语言内的地域差异。 区域设置由小写语言代码指定,可选择后接_和大写地区标识符。 例如"en"代表英语,"en_GB"代表英式英语,"en_US"代表美式英语。 若不清楚所需语言代码,Wikipedia提供详细列表,也可通过stringi::stri_locale_list()查看stringr支持的区域设置。
Base R 的字符串函数会自动使用操作系统设置的区域。 这意味着 base R 字符串函数会按您预期的语言方式工作,但若与不同国家的用户共享代码,其运行结果可能不同。 为避免此问题,stringr 默认采用"en"区域设置(英语规则),需要您显式指定locale参数来覆盖。 幸运的是,有两类函数特别受区域设置影响:大小写转换和排序。
不同语言的大小写转换规则存在差异。 例如土耳其语有两个i:带点和不带点的。 由于这是两个不同的字母,它们的大写形式也不同:
str_to_upper(c("i", "ı"))
#> [1] "I" "I"
str_to_upper(c("i", "ı"), locale = "tr")
#> [1] "İ" "I"
字符串排序取决于字母表顺序,而不同语言的字母表顺序并不相同! 例如在捷克语中,"ch"是一个复合字母,在字母表中排在h之后。
str_sort(c("a", "c", "ch", "h", "z"))
#> [1] "a" "c" "ch" "h" "z"
str_sort(c("a", "c", "ch", "h", "z"), locale = "cs")
#> [1] "a" "c" "h" "ch" "z"
使用dplyr::arrange()进行字符串排序时也会出现这种情况,这就是为什么该函数同样具有locale参数。
14.7 总结
本章您已了解 stringr 包的部分功能:如何创建、组合和提取字符串,以及处理非英语字符串时可能面临的挑战。 现在该学习字符串处理中最重要且强大的工具之一:正则表达式。 正则表达式是一种非常简洁但极具表现力的语言,用于描述字符串中的模式,这将是下一章的主题。
-
或使用 base R 函数
writeLines()。 -
或使用 base R 函数
writeLines()。 -
在 R 4.0.0 及以上版本获取.
-
如果你没有使用 stringr,也可以直接通过
glue::glue()调用该功能。 -
base R 中的等效函数是带有
collapse参数的paste()函数。 -
同样原则适用于
separate_wider_position()和separate_wider_regex()。 -
查看这些条目时,我们推测 babynames 数据可能删除了空格或连字符,并在 15 个字母后进行了截断。
-
此处我使用特殊的
\x将二进制数据直接编码到字符串中。 -
对中文等没有字母系统的语言进行排序则更为复杂。
--------------- 本章结束 ---------------
本期翻译贡献:
@TigerZ生信宝库
注:本文已开启快捷转载,欢迎大家转载,只需标明文章出处即可。