《R for Data Science (2e)》免费中文翻译 (第14章) --- Strings(2)

写在前面

本系列推文为《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_fewtoo_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_okx_piecesx_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) 来提取字符串的部分内容,其中 startend 位置指定了子串的开始和结束点。startend 参数具有包含性,因此返回字符串的长度将为 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 练习

  1. 在计算婴儿名字长度分布时,我们为何使用 wt = n 参数?

  2. 运用 str_length()str_sub() 函数提取每个婴儿名字的中间字母。如果字符串包含偶数个字符,您将如何处理?

  3. 婴儿名字的长度随时间推移是否存在显著趋势?首字母和尾字母的流行度又有哪些变化?

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 包的部分功能:如何创建、组合和提取字符串,以及处理非英语字符串时可能面临的挑战。 现在该学习字符串处理中最重要且强大的工具之一:正则表达式。 正则表达式是一种非常简洁但极具表现力的语言,用于描述字符串中的模式,这将是下一章的主题。


  1. 或使用 base R 函数 writeLines()

  2. 或使用 base R 函数 writeLines()

  3. 在 R 4.0.0 及以上版本获取.

  4. 如果你没有使用 stringr,也可以直接通过glue::glue()调用该功能。

  5. base R 中的等效函数是带有collapse参数的paste()函数。

  6. 同样原则适用于 separate_wider_position()separate_wider_regex()

  7. 查看这些条目时,我们推测 babynames 数据可能删除了空格或连字符,并在 15 个字母后进行了截断。

  8. 此处我使用特殊的\x将二进制数据直接编码到字符串中。

  9. 对中文等没有字母系统的语言进行排序则更为复杂。

--------------- 本章结束 ---------------

本期翻译贡献:

  • @TigerZ生信宝库

注:本文已开启快捷转载,欢迎大家转载,只需标明文章出处即可。

相关推荐
每天学点3 小时前
R语言 使用bibliometrix包进行文献计量学
r语言·文献计量
非著名架构师3 小时前
“低空经济”的隐形护航者:AI驱动的秒级风场探测如何保障无人机物流与城市空管安全?
人工智能·数据分析·疾风气象大模型·高精度天气预报数据·galeweather.cn·高精度气象
洁洁!3 小时前
openEuler在WSL2中的GPU加速AI训练实战指南
人工智能·数据挖掘·数据分析
非著名架构师5 小时前
从“人找信息”到“信息找人”:气象服务模型如何主动推送风险,守护全域安全?
大数据·人工智能·安全·数据分析·高精度天气预报数据·galeweather.cn
clarance20156 小时前
ChatBI王者之争:ThoughtSpot、Databricks、Power BI等五大产品深度对决与选型指南
大数据·人工智能·信息可视化·数据挖掘·数据分析
wind_20677 小时前
二、数据类型
r语言·r语言-4.2.1
梦仔生信进阶7 小时前
【Linux】生物信息学Linux入门指南:从核心命令到实战应用
数据分析
梦想的初衷~7 小时前
基于R语言机器学习遥感数据处理与模型空间预测技术及实际项目案例分析
随机森林·机器学习·r语言
沃达德软件8 小时前
智慧警务实战模型与算法
大数据·人工智能·算法·数据挖掘·数据分析