17 R 정규 표현식(Regular Expression)
정규 표현식(Regular Expression, regex)은 문자열 패턴을 정의하는 문법이다. 정규 표현식은 문자열을 검색하거나 조작하는 데 사용되는 강력한 도구이다. 이 도구는 1970년대 전후로 개발되었으며, 그 이후로 많은 프로그래밍 언어와 텍스트 편집기에서 널리 사용되고 있다.
처음 배울 때는 복잡하고 어려워 보이지만 충분히 배울 가치가 있다. 완전히 암기하고 이해하지 않아도 개념적으로 이해한 후에 ChatGPT 질의 등을 통해서 필요한 패턴을 만들어 사용할 수도 있을 것이다..
다음 예를 보자. stringr
패키지에는 이런 정규 표현식을 사용하여 문자열을 다루는 다양한 함수들이 포함되어 있다.
str_view()
함수는 문자열에서 정규 표현식에 해당하는 부분을 강조하여 보여준다. 위 예에서는 두 번째 인자 "ab+"
는 a
다음에 b
가 1개 이상 있는 경우를 찾는 정규 표현식이다.
str_view()
함수는 이 패턴을 첫 번째 인자로 지정한 문자열 벡터의 각 요소마다 적용하여 패턴에 맞는 경우가 있는 경우 그 부분을 <>
안에 강조하여 보여준다.
17.1 정규표현식을 해석하는 방법
- 정규 표현식은 근본적으로 논리(logic)에 바탕을 둔 도구이기 때문에 논리적으로 생각할 필요가 있다.
- 한 글자씩, 왼쪽에서 오른쪽으로 읽어 나간다.
- 텍스트 파일과 같이 여러 행(line)을 가지고 있는 경우, 하나의 문자열(string)을 대상으로 할지, 하나의 행을 대상으로 할지 주의한다.
- 영어의 경우 대소문자를 구분하는 것이 디폴트이다.
- 글자의 종류와 집합 관계에 주의를 기울인다.
- Characters는 letters와 digits, whitespace, 문장부호 등을 모두 포함한다.
- Letters는
a
,b
,c
… 와A
,B
,C
… 등을 말한다.
- Digits은
0, 1, 2, 3,...9
을 말한다. - Whitespaces는 인쇄되지는 않아도 characters의 일부이라는 점을 기억하자.
- 빈칸(
" "
), 탭(\t
), 줄바꿈(\n
또는\r\n
) 등
- 빈칸(
17.2 정규 표현식의 기초
17.2.1 Character의 종류
정규 표현식에서 문자들 그대로 그 자체가 패턴이 되는 경우를 literal characters라고 한다. 특수한 기능을 하는 문장 부호를 metacharacters라고 한다. Metacharacters에는 다음과 같은 것들이 있으며, 이것들을 제외한 문자, 숫자 등을 literal characters라고 한다.
- Backslash:
\
- Caret:
^
- Dollar sign:
$
- Dot:
.
- Pipe symbol:
|
- Question mark:
?
- Asterisk:
*
- Plus sign:
+
- Opening parenthesis:
(
- Closing parenthesis:
)
- Opening square bracket:
[
- Opening curly brace:
{
그래서 "ab"
라는 패턴은 "a"
와 그 다음에 "b"
가 따라오는 "ab"
에 매칭된다. "ab+
은 +
가 앞의 "b"
가 1개 이상있는 경우를 의미하여 "ab"
, "abb"
, `"abbb"
, "abbbb"
등과 매칭된다.
17.2.2 Charater class: 단 하나의 문자에 매칭
[abc]
는[]
안에 있는 문자 중 하나와 매칭되어,"a"
또는"b"
, 또는"c"
와 매칭된다.[^abc]
는[]
안에^
가 있으면[]
안에 있는 문자 이외의 문자와 매칭된다. 예를 들어,[^a-z]
는 소문자 알파벳이 아닌 문자와 매칭된다(여집합).[]
안에-
을 사용하여 문자 범위를 지정할 수 있다. 예를 들어,[a-z]
는 소문자 알파벳 가운데 하나를 의미하고,[A-Z]
는 대문자 알파벳 가운데 하나를 의미한다.[0-9]
는 숫자 0부터 9까지 가운데 하나를 의미한다. 이런 문자 범위는[a-zA-Z0-9]
와 같이 조합하여 사용할 수 있다. 이 경우는 소문자, 대문자, 숫자 가운데 하나와 매칭된다.
다음은 회색을 의미하는 단어인 gray
와 grey
를 찾는 예이다.
[1] │ I like <gray> color.
[2] │ I like <grey> color.
stringr
패키지에 과일 이름을 요소로 가지는 fruit
문자열 벡터가 있다. 이 가운데 5개만 사용하려고 한다.
fruit5 <- fruit[1:5]
fruit5
[1] "apple" "apricot" "avocado" "banana" "bell pepper"
이들 단어에서 영어의 모음(aeiou
)이 아닌 것을 찾아 보려고 한다.
str_view(fruit5, "[^aeiou]")
[1] │ a<p><p><l>e
[2] │ a<p><r>i<c>o<t>
[3] │ a<v>o<c>a<d>o
[4] │ <b>a<n>a<n>a
[5] │ <b>e<l><l>< ><p>e<p><p>e<r>
17.2.3 단축형 character class
\d
-
\d
는[0-9]
와 같고, 숫자 하나를 의미한다. 그 여집합은\D
이다.
다음은 사용 예시이다.
str_view("031-123-4567", "\\d")
[1] │ <0><3><1>-<1><2><3>-<4><5><6><7>
str_view("031-123-4567", "\\D")
[1] │ 031<->123<->4567
참고로 R에서 \d
를 문자열에서 정규 표현식으로 사용하기 위해서는 앞에 역슬래쉬를 이스케이핑해야 하기 때문에 "\\d"
로 써야 한다.
R 버전 4.0.0
부터는 raw string을 지원한다. raw string은 r"(...)"
문법을 사용한다. Raw string을 사용하는 경우에는 이스케이프를 하지 않아도 된다.
str_view("031-123-4567", r"(\d)")
[1] │ <0><3><1>-<1><2><3>-<4><5><6><7>
str_view("031-123-4567", r"(\D)")
[1] │ 031<->123<->4567
\w
-
\w
는[a-zA-Z0-9_]
와 같고(word를 이루는 문자라는 뜻), 영문자(letter), 숫자(digit), 밑줄 문자(_
) 가운데 하나를 의미한다. 그 여집합은\W
이다.
my_words <- c("abcd", "a bc", "대한민국", "s125", "s_class", "s_*class")
str_view(my_words, r"(\w\w\w\w)")
[1] │ <abcd>
[3] │ <대한민국>
[4] │ <s125>
[5] │ <s_cl>ass
[6] │ s_*<clas>s
\s
-
\s
는 whitespace를 의미하고,[ \t\n\r\f\v]
와 같다. 즉 공백 문자(space), 탭 문자(tab), 줄바꿈 문자(newline), 캐리지 리턴 문자(carriage return), 폼 피드 문자(form feed), 수직 탭 문자(vertical tab) 가운데 하나를 의미한다. 그 여집합은\S
이다.
my_words <- c("abcd", "a bc", "대한민국", "s125", "s_class", "s_*class")
str_view(my_words, r"(\w\s\w)")
[2] │ <a b>c
이런 단축 클래스는 []
에 넣을 수도 있다. 다음은 숫자이거나 -
가운데 하나를 의미한다.
text = "seoul-2024-가-1234"
str_view(text, r"([\d-])")
[1] │ seoul<-><2><0><2><4><->가<-><1><2><3><4>
17.2.4 Dot: .
-
.
은 모든 characters(letters, digits, spaces 등 모두) 가운데 한 글자와 매칭된다. 단, 줄바꿈 문자는 제외된다. 가장 범위가 넓다.
다음은 "..."
패턴을 찾는 예이다. 공백이든 숫자이든 문장부호이든 줄바꿈만 아닌 경우는 모두 3개의 문자와 매칭된다.
[1] │ < >
[2] │ <abc>
[3] │ <123>4
[4] │ <///>
[5] │ < @#>$
[6] │ <대한민>국
[7] │ ab
│ <cde>
다음 경우는 "a"
다음 3개의 문자가 오고 그 다음 "e"
가 오는 경우를 찾는 예이다.
str_view(fruit, "a...e")
[1] │ <apple>
[7] │ bl<ackbe>rry
[48] │ mand<arine>
[51] │ nect<arine>
[62] │ pine<apple>
[64] │ pomegr<anate>
[70] │ r<aspbe>rry
[73] │ sal<al be>rry
.
은 나중에 설명할 *
에 붙여서 ".*"
와 같은 형태를 많이 사용한다. 이 경우는 *
가 0개 이상을 의미하기 때문에 .
과 결합하여 0개 이상의 모든 문자와 매칭된다.
17.2.5 Alternation: 정규식들 가운데 하나와 매칭, |
|
는 정규표현식들 가운데 하나와 매칭된다. 예를 들어 "apple|banana"
는 "apple"
또는 "banana"
와 매칭된다. 다음은 fruit
벡터에서 apple
또는 banana
를 찾는 예이다.
str_view(fruit, "apple|banana")
[1] │ <apple>
[4] │ <banana>
[62] │ pine<apple>
[]
를 사용하여 정의되는 character class는 그 가운데 한 글자와 매칭되는데, |
은 정규식들 가운데 하나와 매칭된다.
다음은 fruit
벡터에서 apple
, banana
, kiwi
를 찾는 예이다.
p <- str_flatten(c("apple", "banana", "kiwi"), "|")
p
[1] "apple|banana|kiwi"
str_view(fruit, p)
[1] │ <apple>
[4] │ <banana>
[42] │ <kiwi> fruit
[62] │ pine<apple>
17.2.6 Anchor: ^
, $
-
^
는 문자열의 시작을 의미한다. 예를 들어"^a"
는"a"
로 시작하는 문자열과 매칭된다. -
$
는 문자열의 끝을 의미한다. 예를 들어"a$"
는"a"
로 끝나는 문자열과 매칭된다.
다음은 fruit
벡터에서 a
로 시작하는 문자열을 찾는 예이다.
str_view(fruit, "^a")
[1] │ <a>pple
[2] │ <a>pricot
[3] │ <a>vocado
다음은 fruit
벡터에서 a
로 끝나는 문자열을 찾는 예이다.
str_view(fruit, "a$")
[4] │ banan<a>
[15] │ cherimoy<a>
[30] │ feijo<a>
[36] │ guav<a>
[56] │ papay<a>
[74] │ satsum<a>
하나의 패턴에서 "^xxx$"
와 같은 형태로 패턴을 정의하면 전체 문자열이 xxx
와 매칭되는지 확인할 수 있다. 다음은 fruit
벡터에서 apple
과 매칭되는 문자열을 찾는 예이다.
str_view(fruit, "^apple$")
[1] │ <apple>
17.2.7 Quantifier
quantifier는 앞의 문자나 패턴이 몇 번 반복되는지를 지정하는 것이다. 다음과 같은 quantifier가 있다.
-
*
: 0개 이상 -
+
: 1개 이상 -
?
: 0개 또는 1개 -
{n}
: n개 -
{n,}
: n개 이상 -
{n,m}
: n개 이상 m개 이하 -
{,m}
: m개 이하
17.2.8 Capturing Group: ()
()
는 괄호 안의 패턴을 그룹으로 묶어서 이것을 다른 곳에서 다시 참조할 수 있은 캡처링 그룹(capturing group)을 만든다. 상대적으로 non-capturing group이라는 개념도 있는데 base R에서 디폴트로 사용하는 PCRE는 non-capturing group을 지원하지 않기 때문이다.
정의된 capturing group을 다시 참조할 때는 \1
, \2
, …과 같이 사용하는 데 이런 것들을 백레퍼런스(backreference)라고 한다.
다음은 fruit
벡터에서 (..)
로 2개의 문자를 그룹으로 묶었다. 이것을 다시 \1
가 뒤에 오기 때문에 그 두 개의 문자가 다시 반복되는 경우를 찾는다.
str_view(fruit, r"((..)\1)")
[4] │ b<anan>a
[20] │ <coco>nut
[22] │ <cucu>mber
[41] │ <juju>be
[56] │ <papa>ya
[73] │ s<alal> berry
다음은 anchor ^
와 $
를 사용했기 때문에 문자열 전체를 대상으로 하는다. (..)
으로 문자 2개를 그룹으로 묶었고, 끝에서 \1
을 다시 사용했다. 그리고 중간에 .*
이 있어서 모든 문자가 된다. 그래서 2개 문자가 앞과 뒤에 반복되는 경우에 매칭된다.
str_view(words, r"(^(..).*\1$)")
[152] │ <church>
[217] │ <decide>
[617] │ <photograph>
[699] │ <require>
[739] │ <sense>
17.2.9 정규 표현식 우선 순위
정규 표현식에도 우선 순위가 중요하다. 다음과 같은 우선 순위가 있다.
-
()
: 괄호 또는 그룹핑 -
*
,+
,?
: Quanatifers - Concatenation: 문자와 문자, 문자와 패턴이 붙어 있는 경우
-
|
: Alternation
그래서 "ab+"
는 다음과 같이 해석된다.
-
+
는 quantifier이기 때문에 앞의"b"
에 딱 붙어서 적용된다. - 그다음
"a"
와"b"
가 붙어 있다(Concatenation).
그래서 "a"
가 있고, 그 뒤에 "b"
가 1개 이상 있는 경우에 ?매칭된다.
그리고, "ab|abc"
라는 패턴을 생각해 보자. 이 경우 concatenation이 먼저 적용되기 때문에 "ab"
와 "abc"
가 각각의 패턴으로 해석된다. 그런 다음 |
가 적용되기 때문에 "ab"
또는 "abc"
와 매칭된다. 그런데 alternation이 적용될 때는 앞에 것이 매칭되면 뒤의 것은 무시된다. 따라서 "ab"
가 매칭되는 경우에는 "abc"
는 매칭되지 않는다.
17.2.10 Flags: 정규 표현식의 작동 방식을 변경하는 옵션
정규 표현식의 작동 방식을 변경하는 옵션을 flags라고 한다.
-
ignore_case = TRUE
: 대소문자를 구분하지 않음 -
dotall = TRUE
:.
이 줄바꿈 문자도 포함하여 모든 문자와 매칭됨 -
multiline = TRUE
: 여러 줄을 대상으로 함
stringr
패키지에는 이런 옵션을 줄 수 있게 regex()
함수를 제공한다. 이 함수 안에 패턴과 이런 옵션을 주면 된다.
정규 표현식은 디폴트로 대소문자를 구분한다. 이것을 무시하게 하는 것이 ignore_case = TRUE
이다.
[1] │ <Apple>
[2] │ <apple>
.
은 디폴트로 줄바꿈 문자를 제외한 모든 문자와 매칭된다. dotall = TRUE
를 주면 줄바꿈 문자도 포함하여 모든 문자와 매칭되게 만든다.
다음에서 .
은 줄바꿈 문자를 제외하기 때문에 매칭되는 문자열이 없다.
str_view("a\nb\nc", "a.")
반면 dotall = TRUE
를 주면 줄바꿈 문자도 포함하여 모든 문자와 매칭되기 때문에 매칭되는 문자열이 있다.
원래 ^
와 $
가 전체 문자열의 시작과 끝을 의미한다. multiline = TRUE
를 주면 각 행의 시작과 끝을 의미하게 된다.
여러 행을 가진 문자열에서 디폴트는 ^
와 $
가 전체 문자열의 시작과 끝을 의미한다.
x <- "Line 1\nLine 2\nLine 3"
str_view(x, "^Line")
[1] │ <Line> 1
│ Line 2
│ Line 3
multiline = TRUE
를 주면 각 행의 시작과 끝을 의미하게 된다.
17.3 stringr
과 정규 표현식의 활용
다음과 같은 Untidy data를 가지고 설명한다. 정규표현식은 이런 Untidy data를 다룰 때 유용하게 사용할 수 있다.
df <- tibble::tribble(
~Id, ~Diagnoses,
"pt1", "DM,HTN,ICH",
"pt2", "DM",
"pt3", "HTN,Obesity,Diabetes",
"pt4", "HTN,DM,Hyperlipidemia",
"pt5", "CI,HTN,Gout",
"pt6", "RA,DM,CAD",
"pt7", "AF,DM, HTN",
"pt8", "AF,HTN,Hyperlipidemia"
)
df
# A tibble: 8 × 2
Id Diagnoses
<chr> <chr>
1 pt1 DM,HTN,ICH
2 pt2 DM
3 pt3 HTN,Obesity,Diabetes
4 pt4 HTN,DM,Hyperlipidemia
5 pt5 CI,HTN,Gout
6 pt6 RA,DM,CAD
7 pt7 AF,DM, HTN
8 pt8 AF,HTN,Hyperlipidemia
17.3.1 문자열의 존재, 개수, 위치 확인하기
위 df
데이터프레임에서 당뇨가 있는 환자의 인원수를 확인하려고 한다. 당연히 Diagnoses
열에 DM
이라는 문자열이 있는지 확인하여 카운트하면 될 것이다. Diabetes
라고 된 데이터는 일단 무시하자. Tidy data라면 아주 간단할 문제이지만 Untidy하기 때문에 약간의 작업이 필요하다.
먼저 str_detect()
함수는 문자열에서 정규 표현식에 해당하는 부분이 있는지를 확인하여 TRUE 또는 FALSE로 반환한다. 문자열 벡터에서 각 요소에서 이런 과정이 반복된다.
library(stringr)
str_detect(df$Diagnoses, "DM")
[1] TRUE TRUE FALSE TRUE FALSE TRUE TRUE FALSE
이것을 sum()
함수와 결합하여 TRUE의 개수를 세면 된다.
sum(str_detect(df$Diagnoses, "DM"))
[1] 5
str_count()
함수는 문자열에서 정규 표현식에 해당하는 부분의 개수를 세어준다.
str_count(df$Diagnoses, "DM")
[1] 1 1 0 1 0 1 1 0
마찬가지로 이것을 sum()
함수와 결합하여 TRUE의 개수를 세면 된다.
어느 환자가 당뇨를 가지고 있는지 확인하고 싶을 수 있다. 이런 경우에는 문자열 벡터에서 정규 표현식에 해당하는 문자열을 가지고 있는 곳의 인덱스를 알려주는 str_which()
함수를 사용하면 된다.
str_which(df$Diagnoses, "DM")
[1] 1 2 4 6 7
이것을 dplyr
패키지의 filter()
함수와 결합하여, str_detect()
함수로 당뇨가 있는 환자만 추출할 수 있다.
library(dplyr)
df |>
filter(str_detect(Diagnoses, "DM"))
# A tibble: 5 × 2
Id Diagnoses
<chr> <chr>
1 pt1 DM,HTN,ICH
2 pt2 DM
3 pt4 HTN,DM,Hyperlipidemia
4 pt6 RA,DM,CAD
5 pt7 AF,DM, HTN
필요에 따라 slice()
함수와 str_which()
함수를 결합하여 사용할 수 있다. slice()
함수는 데이터프레임에서 행을 추출하는 함수이다. 결과는 같다.
17.3.2 문자열 바꾸기
df
데이터프레임에서 당뇨병을 DM
또는 Diabetes
로 표현하고 있더 데이터가 제대로 정리되지 않는다. 이것을 Diabetes
로 통일하고 싶다. 이럴 때는 str_replace()
함수를 사용하여 DM
을 Diabetes
로 바꿔주면 된다.
str_replace(df$Diagnoses, "DM", "Diabetes")
[1] "Diabetes,HTN,ICH" "Diabetes"
[3] "HTN,Obesity,Diabetes" "HTN,Diabetes,Hyperlipidemia"
[5] "CI,HTN,Gout" "RA,Diabetes,CAD"
[7] "AF,Diabetes, HTN" "AF,HTN,Hyperlipidemia"
이런 과정을 반복하면 df
의 진단명을 모두 풀네임으로 바꿀 수도 있을 것이다. ......
str_replace()
함수는 문자열 벡터의 각 요소에 대하여 정규 표현식에 해당하는 부분을 찾아서 바꿔준다. 이때 정규 표현식에 해당하는 부분이 여러 개일 경우에는 첫 번째 것만 바꾼다. 모든 매칭을 바꾸려면 str_replace_all()
함수를 사용한다.
str_replace_all()
함수는 또한 여러 매칭/바꿀값을 한꺼번에 지정할 수도 있다. 단어와 단어 사이에는 _
를 사용하여 붙였다.
str_replace_all(df$Diagnoses,
c("DM" = "Diabetes",
"HTN" = "Hypertension",
"CAG" = "Coronary_Artery_Disease",
"CI" = "Cerebral_Infarct",
"ICH" = "Intracerebral_Hemorrhage",
"Af" = "Atrial_Fibrillation",
"RA" = "Rheumatoid_Arthritis",
"Gout" = "Gouty_Arthritis"))
[1] "Diabetes,Hypertension,Intracerebral_Hemorrhage"
[2] "Diabetes"
[3] "Hypertension,Obesity,Diabetes"
[4] "Hypertension,Diabetes,Hyperlipidemia"
[5] "Cerebral_Infarct,Hypertension,Gouty_Arthritis"
[6] "Rheumatoid_Arthritis,Diabetes,CAD"
[7] "AF,Diabetes, Hypertension"
[8] "AF,Hypertension,Hyperlipidemia"
이 기능을 사용하면 원래의 df
데이터프레임을 바꿀 수 있을 것이다. 이것을 ddf
라는 새로운 데이터프레임에 저장했다.
ddf <- df |>
mutate(
Diagnoses = str_replace_all(Diagnoses,
c("DM" = "Diabetes",
"HTN" = "Hypertension",
"CAG" = "Coronary_Artery_Disease",
"CI" = "Cerebral_Infarct",
"ICH" = "Intracerebral_Hemorrhage",
"AF" = "Atrial_Fibrillation",
"RA" = "Rheumatoid_Arthritis",
"Gout" = "Gouty_Arthritis"))
)
ddf
# A tibble: 8 × 2
Id Diagnoses
<chr> <chr>
1 pt1 Diabetes,Hypertension,Intracerebral_Hemorrhage
2 pt2 Diabetes
3 pt3 Hypertension,Obesity,Diabetes
4 pt4 Hypertension,Diabetes,Hyperlipidemia
5 pt5 Cerebral_Infarct,Hypertension,Gouty_Arthritis
6 pt6 Rheumatoid_Arthritis,Diabetes,CAD
7 pt7 Atrial_Fibrillation,Diabetes, Hypertension
8 pt8 Atrial_Fibrillation,Hypertension,Hyperlipidemia
17.3.3 문자열 나누기
stringr
패키지의 string_split()
함수는 문자열을 정규 표현식에 해당하는 부분을 기준으로 나누어준다. 이 함수를 사용하여 ddf
데이터프레임의 Diagnoses
열을 나누어 보자. 두 번째 인자에 나누는 기준이 되는 정규 표현식을 넣어주면 된다.
str_split(ddf$Diagnoses, ",")
[[1]]
[1] "Diabetes" "Hypertension"
[3] "Intracerebral_Hemorrhage"
[[2]]
[1] "Diabetes"
[[3]]
[1] "Hypertension" "Obesity" "Diabetes"
[[4]]
[1] "Hypertension" "Diabetes" "Hyperlipidemia"
[[5]]
[1] "Cerebral_Infarct" "Hypertension" "Gouty_Arthritis"
[[6]]
[1] "Rheumatoid_Arthritis" "Diabetes" "CAD"
[[7]]
[1] "Atrial_Fibrillation" "Diabetes" " Hypertension"
[[8]]
[1] "Atrial_Fibrillation" "Hypertension" "Hyperlipidemia"
str_split()
함수는 리스트를 반환한다. 단수화한 형태로 반환하시켜련 simplify = TRUE
옵션을 사용한다.
str_split(ddf$Diagnoses, ",", simplify = TRUE)
[,1] [,2] [,3]
[1,] "Diabetes" "Hypertension" "Intracerebral_Hemorrhage"
[2,] "Diabetes" "" ""
[3,] "Hypertension" "Obesity" "Diabetes"
[4,] "Hypertension" "Diabetes" "Hyperlipidemia"
[5,] "Cerebral_Infarct" "Hypertension" "Gouty_Arthritis"
[6,] "Rheumatoid_Arthritis" "Diabetes" "CAD"
[7,] "Atrial_Fibrillation" "Diabetes" " Hypertension"
[8,] "Atrial_Fibrillation" "Hypertension" "Hyperlipidemia"
이렇게 하면 행렬을 반환한다.
그런데 잘 관찰해 보면 7
번 환자의 고혈압 진단이 Hypertension
으로 되어 앞에 공백이 붙어 있다. 이럴 때는 str_trim()
함수를 사용하여 공백을 제거할 수 있다.
stringr
패키지에는 패턴의 행동을 바꾸기 위해 regex()
함수를 사용한다고 했는데, 이와 비슷하게 패턴을 만들때 boundary()
함수를 사용하여 단어의 경계를 정의할 수 있다. boundary("word")
는 단어로 구분된 경계를 의미한다. 이 함수로 패턴을 재정의하여 작업해 보자.
[,1] [,2] [,3]
[1,] "Diabetes" "Hypertension" "Intracerebral_Hemorrhage"
[2,] "Diabetes" "" ""
[3,] "Hypertension" "Obesity" "Diabetes"
[4,] "Hypertension" "Diabetes" "Hyperlipidemia"
[5,] "Cerebral_Infarct" "Hypertension" "Gouty_Arthritis"
[6,] "Rheumatoid_Arthritis" "Diabetes" "CAD"
[7,] "Atrial_Fibrillation" "Diabetes" "Hypertension"
[8,] "Atrial_Fibrillation" "Hypertension" "Hyperlipidemia"
17.3.4 tidyr 패키지를 활용하여 데이터 분리, 추출
앞에서 사용한 ddf
데이터프레임에 새로운 열을 추가하여 (일부러) 다음과 같은 Untidy 데이터프레임을 만들어 보았다. 실제로 이런 방식으로 정리된 데이터를 종종 만나게 된다.
dddf <- ddf |>
mutate(
year_tx = str_c(c("2024", "2015", "2016", "2017", "2018", "2019", "2020", "2021"),
c("A", "A", "A", "A", "B", "B", "B", "B"))
)
dddf
# A tibble: 8 × 3
Id Diagnoses year_tx
<chr> <chr> <chr>
1 pt1 Diabetes,Hypertension,Intracerebral_Hemorrhage 2024A
2 pt2 Diabetes 2015A
3 pt3 Hypertension,Obesity,Diabetes 2016A
4 pt4 Hypertension,Diabetes,Hyperlipidemia 2017A
5 pt5 Cerebral_Infarct,Hypertension,Gouty_Arthritis 2018B
6 pt6 Rheumatoid_Arthritis,Diabetes,CAD 2019B
7 pt7 Atrial_Fibrillation,Diabetes, Hypertension 2020B
8 pt8 Atrial_Fibrillation,Hypertension,Hyperlipidemia 2021B
tidyr
패키지에는 pivot_longer()
와 pivot_wider()
라는 함수 이외에도 위와 같은 데이터를 정리하는 데 도움이 되는 다음 함수들을 가지고 있다.
separate_longer_position()
separate_longer_delim()
separate_wider_position()
separate_wider_delim()
separate_wider_regex()
longer
함수는 데이터를 분리하여 여러 행에 배치하고, wider
함수는 데이터를 분리하여 여러 열에 배치한다. position
함수는 위치에 따라 분리하고, delim
함수는 구분자에 따라 분리한다.
dddf
데이터프레임에서 Diagnoses
열을 ,
로 구분하여 여러 행으로 나누고, year_tx
열은 그대로 두고 싶다. 이럴 때는 separate_longer_delim()
함수를 사용한다.
library(tidyr)
dddf |>
separate_longer_delim(Diagnoses, delim = ",")
# A tibble: 22 × 3
Id Diagnoses year_tx
<chr> <chr> <chr>
1 pt1 Diabetes 2024A
2 pt1 Hypertension 2024A
3 pt1 Intracerebral_Hemorrhage 2024A
4 pt2 Diabetes 2015A
5 pt3 Hypertension 2016A
6 pt3 Obesity 2016A
7 pt3 Diabetes 2016A
8 pt4 Hypertension 2017A
9 pt4 Diabetes 2017A
10 pt4 Hyperlipidemia 2017A
# ℹ 12 more rows
year_tx
열에 대해서는 2개의 열로 나누고 싶다. 이럴 때는 separate_wider_position()
함수를 사용한다. position
함수를 사용할 때는 c()
함수를 사용하여 각 열의 너비(width)를 지정해 주어야 한다.
dddf |>
separate_wider_position(year_tx, c(years = 4, tx = 1))
# A tibble: 8 × 4
Id Diagnoses years tx
<chr> <chr> <chr> <chr>
1 pt1 Diabetes,Hypertension,Intracerebral_Hemorrhage 2024 A
2 pt2 Diabetes 2015 A
3 pt3 Hypertension,Obesity,Diabetes 2016 A
4 pt4 Hypertension,Diabetes,Hyperlipidemia 2017 A
5 pt5 Cerebral_Infarct,Hypertension,Gouty_Arthritis 2018 B
6 pt6 Rheumatoid_Arthritis,Diabetes,CAD 2019 B
7 pt7 Atrial_Fibrillation,Diabetes, Hypertension 2020 B
8 pt8 Atrial_Fibrillation,Hypertension,Hyperlipidemia 2021 B
Diagnoses
열을 ,
로 구분하여 여러 열로 나누고 싶다. 이럴 때는 separate_wider_delim()
함수를 사용한다. 이름을 지정하는 방법과 열의 개수가 맞지 않을 때 처리하는 방법 등은 ?separate_wider_delim
을 참조한다.
dddf |>
separate_wider_delim(Diagnoses, delim = ",",
names = str_c("Diagnosis_", 1:3),
too_few = "align_start")
# A tibble: 8 × 5
Id Diagnosis_1 Diagnosis_2 Diagnosis_3 year_tx
<chr> <chr> <chr> <chr> <chr>
1 pt1 Diabetes Hypertension "Intracerebral_Hemorrhage" 2024A
2 pt2 Diabetes <NA> <NA> 2015A
3 pt3 Hypertension Obesity "Diabetes" 2016A
4 pt4 Hypertension Diabetes "Hyperlipidemia" 2017A
5 pt5 Cerebral_Infarct Hypertension "Gouty_Arthritis" 2018B
6 pt6 Rheumatoid_Arthritis Diabetes "CAD" 2019B
7 pt7 Atrial_Fibrillation Diabetes " Hypertension" 2020B
8 pt8 Atrial_Fibrillation Hypertension "Hyperlipidemia" 2021B
17.3.5 데이터 추출
dddf
데이터프레임에서 year_tx
열의 연도만 추출하고 싶다. 이럴 때는 str_extract()
함수를 사용하여 정규 표현식에 해당하는 부분을 추출할 수 있다.
다음 정규 표현식은 숫자 4개에 매칭된다. \d
는 숫자 하나를 의미하고, {4}
는 숫자가 4개라는 뜻이다.
str_extract(dddf$year_tx, "^\\d{4}")
[1] "2024" "2015" "2016" "2017" "2018" "2019" "2020" "2021"
dddf |>
mutate(
year = str_extract(year_tx, "^\\d{4}")
)
# A tibble: 8 × 4
Id Diagnoses year_tx year
<chr> <chr> <chr> <chr>
1 pt1 Diabetes,Hypertension,Intracerebral_Hemorrhage 2024A 2024
2 pt2 Diabetes 2015A 2015
3 pt3 Hypertension,Obesity,Diabetes 2016A 2016
4 pt4 Hypertension,Diabetes,Hyperlipidemia 2017A 2017
5 pt5 Cerebral_Infarct,Hypertension,Gouty_Arthritis 2018B 2018
6 pt6 Rheumatoid_Arthritis,Diabetes,CAD 2019B 2019
7 pt7 Atrial_Fibrillation,Diabetes, Hypertension 2020B 2020
8 pt8 Atrial_Fibrillation,Hypertension,Hyperlipidemia 2021B 2021
# A tibble: 3 × 1
x
<chr>
1 202215TX
2 202122LA
3 202325CA
some_df
데이터프레임에서 x
열에서 앞 4자리는 연도, 다음 2자리는 나이, 다음 2개는 장소라고 생각해 보자. str_extract()
함수를 사용하여 나이를 추출해 보자.
다음은 정규 표현식에서 (?<=...)
앞에서 설명하지 않은 *positive lookbehind** 문법이고 , (...)
안에 있는 패턴이 앞에 있어야 한다는 뜻이다. 즉, (?<=\\d{4})
는 숫자 4개가 앞에 있어야 한다는 뜻이다. (?<=\\d{4})\\d{2}
는 숫자 4개가 앞에 있고, 그 다음에 숫자 2개가 오는 경우를 의미한다.
str_extract(some_df$x, "(?<=\\d{4})\\d{2}")
[1] "15" "22" "25"
물론 앞에서 tidyr
의 separate_wider_position()
함수를 사용하여 나이를 추출할 수도 있다.
some_df |>
separate_wider_position(x, c(year = 4, age = 2, place = 2))
# A tibble: 3 × 3
year age place
<chr> <chr> <chr>
1 2022 15 TX
2 2021 22 LA
3 2023 25 CA
17.4 문자열 정렬
stringr
패키지의 str_sort()
함수로 문자열을 정렬한다.
[1] "광주" "대구" "대전" "부산" "서울" "수원" "울산" "인천"
다음은 내림차순으로 정렬하는 예이다.
str_sort(places, decreasing = TRUE)
[1] "인천" "울산" "수원" "서울" "부산" "대전" "대구" "광주"
17.5 정리
정규 표현식은 문자열을 다룰 때 매우 유용한 도구이다. 정규 표현식을 사용하여 문자열을 검색하고, 바꾸고, 나누고, 추출하는 등의 작업을 수행할 수 있다. stringr
패키지는 정규 표현식을 쉽게 사용할 수 있도록 다양한 함수를 제공한다. 다음 cheatsheet에서 stringr
과 정규 표현식이 잘 정리되어 있다.
좀 더 강력한 정규 표현식을 사용하고 싶다면 구글의 re2
패키지를 고민할 수 있다.