14  문자열

14.1 소개

지금까지 자세한 내용은 많이 배우지 않고 많은 문자열을 사용했습니다. 이제 문자열을 자세히 살펴보고, 문자열이 어떻게 작동하는지 배우고, 자유롭게 사용할 수 있는 강력한 문자열 조작 도구를 마스터할 때입니다.

문자열과 문자형 벡터를 만드는 세부 사항부터 시작하겠습니다. 그런 다음 데이터에서 문자열을 만드는 방법과 반대로 데이터에서 문자열을 추출하는 방법을 알아볼 것입니다. 그 다음 개별 문자로 작업하는 도구에 대해 논의할 것입니다. 이 장은 개별 문자로 작업하는 함수와 다른 언어로 작업할 때 영어에 대한 기대가 잘못될 수 있는 부분에 대한 간단한 논의로 마무리됩니다.

다음 장에서도 문자열 작업을 계속할 것이며, 거기서는 정규 표현식의 힘에 대해 더 배우게 될 것입니다.

14.1.1 선수 지식

이 장에서는 핵심 tidyverse의 일부인 stringr 패키지의 함수를 사용할 것입니다. 또한 조작할 재미있는 문자열을 제공하는 babynames 데이터도 사용할 것입니다.

library(tidyverse)
#> Warning: package 'ggplot2' was built under R version 4.5.2
#> Warning: package 'readr' was built under R version 4.5.2
library(babynames)

모든 stringr 함수는 str_로 시작하기 때문에 stringr 함수를 사용하고 있는지 금방 알 수 있습니다. 이것은 RStudio를 사용하는 경우 특히 유용한데, str_을 입력하면 자동 완성이 트리거되어 사용 가능한 함수에 대한 기억을 되살릴 수 있기 때문입니다.

RStudio 콘솔에 str_c가 입력되어 있고 상단에 자동 완성 툴팁이 표시되어  str_c로 시작하는 함수들이 나열되어 있습니다. 자동 완성 목록에서 강조  표시된 함수의 시그니처와 매뉴얼 페이지 시작 부분이 오른쪽에 있는 패널에  표시됩니다.

14.2 문자열 만들기

책의 앞부분에서 문자열을 스치듯 만들었지만 세부 사항은 논의하지 않았습니다. 먼저 작은따옴표(') 또는 큰따옴표(")를 사용하여 문자열을 만들 수 있습니다. 두 가지의 동작에는 차이가 없으므로 일관성을 위해 tidyverse 스타일 가이드에서는 문자열에 여러 개의 "가 포함되어 있지 않는 한 "를 사용할 것을 권장합니다.

string1 <- "This is a string"
string2 <- 'If I want to include a "quote" inside a string, I use single quotes'

따옴표를 닫는 것을 잊어버리면 연속 프롬프트인 +가 표시됩니다:

> "This is a string without a closing quote
+ 
+ 
+ HELP I'M STUCK IN A STRING

이런 일이 발생하고 어떤 따옴표를 닫아야 할지 모르겠다면 Escape 키를 눌러 취소하고 다시 시도하세요.

14.2.1 이스케이프(Escapes)

문자열에 리터럴 작은따옴표나 큰따옴표를 포함하려면 \를 사용하여 “이스케이프”할 수 있습니다:

double_quote <- "\"" # or '"'
single_quote <- '\'' # or "'"

따라서 문자열에 리터럴 백슬래시를 포함하려면 이스케이프해야 합니다: "\\":

backslash <- "\\"

문자열의 인쇄된 표현은 이스케이프를 보여주기 때문에 문자열 자체와 동일하지 않다는 점에 유의하세요(즉, 문자열을 인쇄할 때 출력을 복사하여 붙여넣으면 해당 문자열을 다시 만들 수 있습니다). 문자열의 원시 내용을 보려면 str_view()1를 사용하세요:

x <- c(single_quote, double_quote, backslash)
x
#> [1] "'"  "\"" "\\"
str_view(x)
#> [1] │ '
#> [2] │ "
#> [3] │ \

14.2.2 원시 문자열(Raw strings)

여러 따옴표나 백슬래시로 문자열을 만드는 것은 금방 헷갈립니다. 문제를 설명하기 위해 double_quotesingle_quote 변수를 정의한 코드 블록의 내용을 포함하는 문자열을 만들어 보겠습니다:

tricky <- "double_quote <- \"\\\"\" # or '\"'
single_quote <- '\\'' # or \"'\""
str_view(tricky)
#> [1] │ double_quote <- "\"" # or '"'
#>     │ single_quote <- '\'' # or "'"

백슬래시가 정말 많네요! (이것을 때때로 기우는 이쑤시개 증후군이라고 합니다.) 이스케이프를 제거하려면 대신 원시 문자열2을 사용할 수 있습니다:

tricky <- r"(double_quote <- "\"" # or '"'
single_quote <- '\'' # or "'")"
str_view(tricky)
#> [1] │ double_quote <- "\"" # or '"'
#>     │ single_quote <- '\'' # or "'"

원시 문자열은 일반적으로 r"("로 시작하고 )"로 끝납니다. 그러나 문자열에 )"가 포함되어 있는 경우 대신 r"[]" 또는 r"{}"를 사용할 수 있으며, 그래도 충분하지 않은 경우 대시를 원하는 만큼 삽입하여 열고 닫는 쌍을 고유하게 만들 수 있습니다. 예: r"--()--", r"---()---" 등. 원시 문자열은 모든 텍스트를 처리할 수 있을 만큼 유연합니다.

14.2.3 기타 특수 문자

", ', \ 외에도 유용하게 사용할 수 있는 몇 가지 다른 특수 문자가 있습니다. 가장 일반적인 것은 \n(새 줄)과 \t(탭)입니다. 또한 \u 또는 \U로 시작하는 유니코드 이스케이프가 포함된 문자열을 볼 수도 있습니다. 이것은 모든 시스템에서 작동하는 영어가 아닌 문자를 쓰는 방법입니다. ?Quotes에서 다른 특수 문자의 전체 목록을 볼 수 있습니다.

x <- c("one\ntwo", "one\ttwo", "\u00b5", "\U0001f604")
x
#> [1] "one\ntwo" "one\ttwo" "µ"        "😄"
str_view(x)
#> [1] │ one
#>     │ two
#> [2] │ one{\t}two
#> [3] │ µ
#> [4] │ 😄

str_view()는 탭을 쉽게 찾을 수 있도록 중괄호를 사용합니다3. 텍스트 작업의 어려움 중 하나는 텍스트에 공백이 포함될 수 있는 방법이 다양하다는 것인데, 이러한 배경 지식은 뭔가 이상한 일이 일어나고 있음을 인식하는 데 도움이 됩니다.

14.2.4 연습문제

  1. 다음 값을 포함하는 문자열을 만드세요:

    1. He said "That's amazing!"

    2. \a\b\c\d

    3. \\\\\

  2. R 세션에서 문자열을 만들고 인쇄하세요. 특수 문자 “0a0”은 어떻게 됩니까? str_view()는 어떻게 표시합니까? 이 특수 문자가 무엇인지 구글링해 볼 수 있습니까?

    x <- "This\u00a0is\u00a0tricky"

14.3 데이터에서 많은 문자열 만들기

이제 “손으로” 문자열 한두 개를 만드는 기본 사항을 배웠으므로 다른 문자열에서 문자열을 만드는 세부 사항으로 들어갑니다. 이것은 작성한 텍스트를 데이터 프레임의 문자열과 결합하려는 일반적인 문제를 해결하는 데 도움이 됩니다. 예를 들어 “Hello”와 name 변수를 결합하여 인사를 만들 수 있습니다. str_c()str_glue()를 사용하여 이를 수행하는 방법과 mutate()와 함께 사용하는 방법을 보여줄 것입니다. 그러면 자연스럽게 summarize()와 함께 어떤 stringr 함수를 사용할 수 있는지에 대한 질문이 제기되므로 문자열 요약 함수인 str_flatten()에 대한 논의로 이 섹션을 마무리하겠습니다.

14.3.1 str_c()

str_c()는 임의의 수의 벡터를 인수로 받아 문자 벡터를 반환합니다:

str_c("x", "y")
#> [1] "xy"
str_c("x", "y", "z")
#> [1] "xyz"
str_c("Hello ", c("John", "Susan"))
#> [1] "Hello John"  "Hello Susan"

str_c()는 기본 paste0()과 매우 유사하지만 재활용 및 결측값 전파에 대한 일반적인 tidyverse 규칙을 준수하여 mutate()와 함께 사용되도록 설계되었습니다:

df <- tibble(name = c("Flora", "David", "Terra", NA))
df |> mutate(greeting = str_c("Hi ", name, "!"))
#> # A tibble: 4 × 2
#>   name  greeting 
#>   <chr> <chr>    
#> 1 Flora Hi Flora!
#> 2 David Hi David!
#> 3 Terra Hi Terra!
#> 4 <NA>  <NA>

결측값을 다른 방식으로 표시하려면 coalesce()를 사용하여 대체하세요. 원하는 것에 따라 str_c() 내부 또는 외부에서 사용할 수 있습니다:

df |> 
  mutate(
    greeting1 = str_c("Hi ", coalesce(name, "you"), "!"),
    greeting2 = coalesce(str_c("Hi ", name, "!"), "Hi!")
  )
#> # A tibble: 4 × 3
#>   name  greeting1 greeting2
#>   <chr> <chr>     <chr>    
#> 1 Flora Hi Flora! Hi Flora!
#> 2 David Hi David! Hi David!
#> 3 Terra Hi Terra! Hi Terra!
#> 4 <NA>  Hi you!   Hi!

14.3.2 str_glue()

str_c()로 많은 고정 문자열과 가변 문자열을 섞는다면 "를 많이 입력하게 되어 코드의 전체 목표를 보기가 어렵습니다. glue 패키지에서 str_glue()4를 통해 대안적인 접근 방식을 제공합니다. 특별한 기능이 있는 단일 문자열을 제공합니다: {} 내부의 모든 것은 따옴표 밖에 있는 것처럼 평가됩니다:

df |> mutate(greeting = str_glue("Hi {name}!"))
#> # A tibble: 4 × 2
#>   name  greeting 
#>   <chr> <glue>   
#> 1 Flora Hi Flora!
#> 2 David Hi David!
#> 3 Terra Hi Terra!
#> 4 <NA>  Hi NA!

보시다시피 str_glue()는 현재 결측값을 문자열 "NA"로 변환하므로 불행히도 str_c()와 일관성이 없습니다.

문자열에 일반 { 또는 }를 포함해야 하는 경우 어떻게 되는지 궁금할 수도 있습니다. 어떻게든 이스케이프해야 한다고 추측했다면 올바른 방향입니다. 비결은 glue가 약간 다른 이스케이프 기술을 사용한다는 것입니다: \와 같은 특수 문자를 접두사로 붙이는 대신 특수 문자를 두 배로 늘립니다:

df |> mutate(greeting = str_glue("{{Hi {name}!}}"))
#> # A tibble: 4 × 2
#>   name  greeting   
#>   <chr> <glue>     
#> 1 Flora {Hi Flora!}
#> 2 David {Hi David!}
#> 3 Terra {Hi Terra!}
#> 4 <NA>  {Hi NA!}

14.3.3 str_flatten()

str_c()str_glue()는 출력이 입력과 길이가 같기 때문에 mutate()와 잘 작동합니다. summarize()와 잘 작동하는 함수, 즉 항상 단일 문자열을 반환하는 함수를 원한다면 어떻게 해야 할까요? 그것이 str_flatten()5의 역할입니다: 문자 벡터를 받아 벡터의 각 요소를 단일 문자열로 결합합니다:

str_flatten(c("x", "y", "z"))
#> [1] "xyz"
str_flatten(c("x", "y", "z"), ", ")
#> [1] "x, y, z"
str_flatten(c("x", "y", "z"), ", ", last = ", and ")
#> [1] "x, y, and z"

이것은 summarize()와 잘 작동하게 만듭니다:

df <- tribble(
  ~ name, ~ fruit,
  "Carmen", "banana",
  "Carmen", "apple",
  "Marvin", "nectarine",
  "Terence", "cantaloupe",
  "Terence", "papaya",
  "Terence", "mandarin"
)
df |>
  group_by(name) |> 
  summarize(fruits = str_flatten(fruit, ", "))
#> # A tibble: 3 × 2
#>   name    fruits                      
#>   <chr>   <chr>                       
#> 1 Carmen  banana, apple               
#> 2 Marvin  nectarine                   
#> 3 Terence cantaloupe, papaya, mandarin

14.3.4 연습문제

  1. 다음 입력에 대해 paste0()str_c()의 결과를 비교하고 대조하세요:

    str_c("hi ", NA)
    str_c(letters[1:2], letters[1:3])
  2. paste()paste0()의 차이점은 무엇입니까? str_c()paste()와 동등한 것을 어떻게 재현할 수 있습니까?

  3. 다음 표현식을 str_c()에서 str_glue()로 또는 그 반대로 변환하세요:

    1. str_c("The price of ", food, " is ", price)

    2. str_glue("I'm {age} years old and live in {country}")

    3. str_c("\\section{", title, "}")

14.4 문자열에서 데이터 추출

여러 변수가 하나의 문자열에 함께 섞여 있는 것은 매우 일반적입니다. 이 섹션에서는 이를 추출하기 위해 네 가지 tidyr 함수를 사용하는 방법을 배웁니다:

  • df |> separate_longer_delim(col, delim)
  • df |> separate_longer_position(col, width)
  • df |> separate_wider_delim(col, delim, names)
  • df |> separate_wider_position(col, widths)

자세히 보면 여기에 공통 패턴이 있음을 알 수 있습니다: separate_, 그 다음 longer 또는 wider, 그 다음 _, 그 다음 delim 또는 position으로. 이는 이 네 가지 함수가 두 가지 더 단순한 기본 요소로 구성되어 있기 때문입니다:

  • pivot_longer()pivot_wider()와 마찬가지로 _longer 함수는 새 행을 생성하여 입력 데이터 프레임을 길게 만들고 _wider 함수는 새 열을 생성하여 입력 데이터 프레임을 넓게 만듭니다.
  • delim", " 또는 " "와 같은 구분 기호로 문자열을 분할합니다. positionc(3, 5, 2)와 같이 지정된 너비로 분할합니다.

Chapter 15 에서 이 제품군의 마지막 멤버인 separate_wider_regex()로 돌아올 것입니다. 이것은 wider 함수 중 가장 유연하지만 사용하기 전에 정규 표현식에 대해 알고 있어야 합니다.

다음 두 섹션에서는 이러한 분리 함수 뒤에 있는 기본 아이디어를 제공할 것입니다. 먼저 행으로 분리(조금 더 간단함)하고 그 다음 열로 분리합니다. wider 함수가 문제를 진단하기 위해 제공하는 도구에 대해 논의하며 마무리하겠습니다.

14.4.1 행으로 분리

문자열을 행으로 분리하는 것은 구성 요소의 수가 행마다 다를 때 가장 유용한 경향이 있습니다. 가장 일반적인 경우는 구분 기호를 기반으로 분할하기 위해 separate_longer_delim()이 필요한 경우입니다:

df1 <- tibble(x = c("a,b,c", "d,e", "f"))
df1 |> 
  separate_longer_delim(x, delim = ",")
#> # A tibble: 6 × 1
#>   x    
#>   <chr>
#> 1 a    
#> 2 b    
#> 3 c    
#> 4 d    
#> 5 e    
#> 6 f

야생에서 separate_longer_position()을 보는 것은 더 드물지만, 일부 오래된 데이터셋은 각 문자가 값을 기록하는 데 사용되는 매우 간결한 형식을 사용합니다:

df2 <- tibble(x = c("1211", "131", "21"))
df2 |> 
  separate_longer_position(x, width = 1)
#> # A tibble: 9 × 1
#>   x    
#>   <chr>
#> 1 1    
#> 2 2    
#> 3 1    
#> 4 1    
#> 5 1    
#> 6 3    
#> # ℹ 3 more rows

14.4.2 열로 분리

문자열을 열로 분리하는 것은 각 문자열에 고정된 수의 구성 요소가 있고 이를 열로 펼치고 싶을 때 가장 유용한 경향이 있습니다. 열 이름을 지정해야 하기 때문에 longer 등가물보다 약간 더 복잡합니다. 예를 들어 다음 데이터셋에서 x.로 구분된 코드, 에디션 번호, 연도로 구성됩니다. separate_wider_delim()을 사용하려면 두 인수에서 구분 기호와 이름을 제공합니다:

df3 <- tibble(x = c("a10.1.2022", "b10.2.2011", "e15.1.2015"))
df3 |> 
  separate_wider_delim(
    x,
    delim = ".",
    names = c("code", "edition", "year")
  )
#> # A tibble: 3 × 3
#>   code  edition year 
#>   <chr> <chr>   <chr>
#> 1 a10   1       2022 
#> 2 b10   2       2011 
#> 3 e15   1       2015

특정 조각이 유용하지 않은 경우 NA 이름을 사용하여 결과에서 생략할 수 있습니다:

df3 |> 
  separate_wider_delim(
    x,
    delim = ".",
    names = c("code", NA, "year")
  )
#> # A tibble: 3 × 2
#>   code  year 
#>   <chr> <chr>
#> 1 a10   2022 
#> 2 b10   2011 
#> 3 e15   2015

separate_wider_position()은 일반적으로 각 열의 너비를 지정하기 때문에 약간 다르게 작동합니다. 따라서 이름은 새 열의 이름을 제공하고 값은 차지하는 문자 수인 명명된 정수 벡터를 제공합니다. 이름을 지정하지 않아 출력에서 값을 생략할 수 있습니다:

df4 <- tibble(x = c("202215TX", "202122LA", "202325CA")) 
df4 |> 
  separate_wider_position(
    x,
    widths = c(year = 4, age = 2, state = 2)
  )
#> # A tibble: 3 × 3
#>   year  age   state
#>   <chr> <chr> <chr>
#> 1 2022  15    TX   
#> 2 2021  22    LA   
#> 3 2023  25    CA

14.4.3 넓히기 문제 진단

separate_wider_delim()6은 고정되고 알려진 열 집합을 필요로 합니다. 일부 행에 예상된 수의 조각이 없으면 어떻게 될까요? 조각이 너무 적거나 너무 많은 두 가지 가능한 문제가 있으므로 separate_wider_delim()은 도움이 되는 두 가지 인수 too_fewtoo_many를 제공합니다. 다음 샘플 데이터셋으로 too_few 케이스를 먼저 살펴보겠습니다:

df <- tibble(a = c("1-1-1", "1-1-2", "1-3", "1-3-2", "1"))

df |> 
  separate_wider_delim(
    a,
    delim = "-",
    names = c("x", "y", "z")
  )
#> Error in `separate_wider_delim()`:
#> ! Expected 3 pieces in each element of `a`.
#> ! 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(
    a,
    delim = "-",
    names = c("x", "y", "z"),
    too_few = "debug"
  )
#> Warning: Debug mode activated: adding variables `a_ok`, `a_pieces`, and
#> `a_remainder`.
debug
#> # A tibble: 5 × 7
#>   x     y     z     a     a_ok  a_pieces a_remainder
#>   <chr> <chr> <chr> <chr> <lgl>    <int> <chr>      
#> 1 1     1     1     1-1-1 TRUE         3 ""         
#> 2 1     1     2     1-1-2 TRUE         3 ""         
#> 3 1     3     <NA>  1-3   FALSE        2 ""         
#> 4 1     3     2     1-3-2 TRUE         3 ""         
#> 5 1     <NA>  <NA>  1     FALSE        1 ""

디버그 모드를 사용하면 출력에 a_ok, a_pieces, a_remainder라는 세 개의 추가 열이 추가됩니다(다른 이름의 변수를 분리하면 다른 접두사가 붙습니다). 여기서 a_ok를 사용하면 실패한 입력을 빠르게 찾을 수 있습니다:

debug |> filter(!a_ok)
#> # A tibble: 2 × 7
#>   x     y     z     a     a_ok  a_pieces a_remainder
#>   <chr> <chr> <chr> <chr> <lgl>    <int> <chr>      
#> 1 1     3     <NA>  1-3   FALSE        2 ""         
#> 2 1     <NA>  <NA>  1     FALSE        1 ""

a_pieces는 예상되는 3(names의 길이)과 비교하여 몇 개의 조각이 발견되었는지 알려줍니다. a_remainder는 조각이 너무 적을 때는 유용하지 않지만 잠시 후에 다시 보게 될 것입니다.

이 디버깅 정보를 보면 구분 기호 전략에 문제가 있거나 분리하기 전에 전처리를 더 해야 한다는 것을 알 수 있습니다. 이 경우 업스트림에서 문제를 수정하고 too_few = "debug"를 제거하여 새로운 문제가 오류가 되도록 하세요.

다른 경우에는 누락된 조각을 NA로 채우고 계속 진행하고 싶을 수 있습니다. too_few = "align_start"too_few = "align_end"NA가 어디로 가야 하는지 제어할 수 있게 해줍니다:

df |> 
  separate_wider_delim(
    a,
    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(a = c("1-1-1", "1-1-2", "1-3-5-6", "1-3-2", "1-3-5-7-9"))

df |> 
  separate_wider_delim(
    a,
    delim = "-",
    names = c("x", "y", "z")
  )
#> Error in `separate_wider_delim()`:
#> ! Expected 3 pieces in each element of `a`.
#> ! 2 values were too long.
#> ℹ Use `too_many = "debug"` to diagnose the problem.
#> ℹ Use `too_many = "drop"/"merge"` to silence this message.

하지만 이제 결과를 디버깅할 때 a_remainder의 목적을 볼 수 있습니다:

debug <- df |> 
  separate_wider_delim(
    a,
    delim = "-",
    names = c("x", "y", "z"),
    too_many = "debug"
  )
#> Warning: Debug mode activated: adding variables `a_ok`, `a_pieces`, and
#> `a_remainder`.
debug |> filter(!a_ok)
#> # A tibble: 2 × 7
#>   x     y     z     a         a_ok  a_pieces a_remainder
#>   <chr> <chr> <chr> <chr>     <lgl>    <int> <chr>      
#> 1 1     3     5     1-3-5-6   FALSE        4 -6         
#> 2 1     3     5     1-3-5-7-9 FALSE        5 -7-9

너무 많은 조각을 처리하기 위한 약간 다른 옵션 세트가 있습니다: 추가 조각을 조용히 “삭제(drop)”하거나 모두 최종 열로 “병합(merge)”할 수 있습니다:

df |> 
  separate_wider_delim(
    a,
    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(
    a,
    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 문자(Letters)

이 섹션에서는 문자열 내의 개별 문자로 작업할 수 있는 함수를 소개합니다. 문자열의 길이를 찾고, 하위 문자열을 추출하고, 플롯과 테이블에서 긴 문자열을 처리하는 방법을 배웁니다.

14.5.1 길이

str_length()는 문자열의 문자 수를 알려줍니다:

str_length(c("a", "R for data science", NA))
#> [1]  1 18 NA

이것을 count()와 함께 사용하여 미국 아기 이름 길이의 분포를 찾은 다음 filter()와 함께 사용하여 가장 긴 이름(우연히 15자임)을 볼 수 있습니다7:

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

이 6개의 16진수 각각은 하나의 글자를 나타냅니다. 48은 H, 61은 a 등입니다. 16진수에서 문자로의 매핑을 인코딩이라고 하며, 이 경우 인코딩은 ASCII라고 합니다. ASCII는 미국 정보 교환 표준 코드(American Standard Code for Information Interchange)이기 때문에 영어 문자를 표현하는 데 훌륭합니다.

영어 이외의 언어에서는 상황이 그렇게 쉽지 않습니다. 컴퓨팅 초기에는 영어가 아닌 문자를 인코딩하기 위한 많은 경쟁 표준이 있었습니다. 예를 들어 유럽에는 두 가지 다른 인코딩이 있었습니다. 라틴1(일명 ISO-8859-1)은 서유럽 언어에 사용되었고 라틴2(일명 ISO-8859-2)는 중부 유럽 언어에 사용되었습니다. 라틴1에서 바이트 b1은 “±”이지만 라틴2에서는 “ą”입니다! 다행히 오늘날에는 거의 모든 곳에서 지원되는 하나의 표준이 있습니다: UTF-8. UTF-8은 오늘날 인간이 사용하는 거의 모든 문자와 이모티콘과 같은 많은 추가 기호를 인코딩할 수 있습니다.

readr은 모든 곳에서 UTF-8을 사용합니다. 이것은 좋은 기본값이지만 UTF-8을 사용하지 않는 오래된 시스템에서 생성된 데이터에 대해서는 실패할 것입니다. 이런 일이 발생하면 문자열을 인쇄할 때 이상하게 보일 것입니다. 때로는 한두 개의 문자가 엉망이 될 수 있고, 때로는 완전한 횡설수설을 얻을 수 있습니다. 예를 들어 비정상적인 인코딩을 가진 두 개의 인라인 CSV가 있습니다8:

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 문자 변형

악센트가 있는 언어로 작업하는 것은 악센트가 있는 문자가 단일 개별 문자(예: ü)로 인코딩되거나 악센트가 없는 문자(예: u)와 분음 부호(예: ¨)를 결합하여 두 문자로 인코딩될 수 있으므로 문자의 위치를 결정할 때(예: str_length()str_sub() 사용) 상당한 어려움을 줍니다. 예를 들어 이 코드는 똑같이 보이는 ü를 표현하는 두 가지 방법을 보여줍니다:

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 로케일 의존 함수

마지막으로 동작이 로케일(locale) 에 따라 달라지는 소수의 stringr 함수가 있습니다. 로케일은 언어와 유사하지만 언어 내의 지역적 변형을 처리하기 위한 선택적 지역 지정자를 포함합니다. 로케일은 소문자 언어 약어로 지정되며 선택적으로 _와 대문자 지역 식별자가 뒤따릅니다. 예를 들어 “en”은 영어, “en_GB”는 영국 영어, “en_US”는 미국 영어입니다. 언어 코드를 아직 모른다면 위키백과에 좋은 목록이 있으며 stringi::stri_locale_list()를 보면 stringr에서 지원되는 코드를 볼 수 있습니다.

기본 R 문자열 함수는 운영 체제에서 설정한 로케일을 자동으로 사용합니다. 즉, 기본 R 문자열 함수는 해당 언어에 대해 예상하는 작업을 수행하지만 다른 국가에 사는 사람과 코드를 공유하면 코드가 다르게 작동할 수 있습니다. 이 문제를 피하기 위해 stringr은 “en” 로케일을 사용하여 영어 규칙을 기본값으로 사용하며 이를 재정의하려면 locale 인수를 지정해야 합니다. 다행히 로케일이 정말 중요한 함수 세트는 대소문자 변경과 정렬 두 가지뿐입니다.

대소문자 변경 규칙은 언어마다 다릅니다. 예를 들어 터키어에는 점이 있는 i와 점이 없는 i 두 가지가 있습니다. 두 개의 별도 문자이므로 대문자로 다르게 표기됩니다:

str_to_upper(c("i", "ı"))
#> [1] "I" "I"
str_to_upper(c("i", "ı"), locale = "tr")
#> [1] "İ" "I"

문자열 정렬은 알파벳 순서에 따라 달라지며 알파벳 순서는 모든 언어에서 동일하지 않습니다9! 예를 들어 체코어에서 “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. 또는 기본 R 함수 writeLines()를 사용하세요.↩︎

  2. R 4.0.0 이상에서 사용 가능합니다.↩︎

  3. str_view()는 또한 색상을 사용하여 탭, 공백, 일치 항목 등을 강조합니다. 색상은 현재 책에 표시되지 않지만 대화식으로 코드를 실행할 때 알 수 있습니다.↩︎

  4. stringr을 사용하지 않는 경우 glue::glue()를 사용하여 직접 액세스할 수도 있습니다.↩︎

  5. 기본 R의 등가물은 collapse 인수와 함께 사용되는 paste()입니다.↩︎

  6. 동일한 원칙이 separate_wider_position()separate_wider_regex()에도 적용됩니다.↩︎

  7. 이 항목들을 보면 babynames 데이터가 공백이나 하이픈을 삭제하고 15자 이후를 자른다고 추측할 수 있습니다.↩︎

  8. 여기서는 특수 \x를 사용하여 이진 데이터를 문자열로 직접 인코딩하고 있습니다.↩︎

  9. 중국어와 같이 알파벳이 없는 언어의 정렬은 훨씬 더 복잡합니다.↩︎