聰明不如鈍筆
총명불여둔필
assignment KindeR

R에서 특정 문자열 포함 여부 깔끔하게 확인하기(feat. str_detect)


이번에도 페이스북 R 스터디 그룹에 올라온 질문입니다.


도와주세요.ㅠㅠ


x1, x2, x3의 컬럼을 가진 데이터 프레임에서 x1 컬럼에 'abc'를 포함하고 있으면 x3에 'yes'를 채우고, x1에 컬럼에 'def'를 포함하고 있으면 x3에 'no'를 채워 넣고 싶은데, 어떤 방법이 있을까요?


'abc', 'def'의 글자 위치가 각각 달라서 substr(x, start, stop) 같은 함수를 쓸 수가 없군요.


또 filter나 select 같은 함수는 행을 추출해버리고요.


이 블로그를 꾸준히 읽어오신 분이라면 '최대한 친절하게 쓴 R로 낱말구름, 의미연결망 그리기(feat. tidyverse, KoNLP)' 포스트가 떠오르셨을 겁니다.


이 포스트에서는 문장에 품사를 부착한 다음 str_match() 함수를 활용해 한글 명사만 골라냈습니다.


str_match()는 기본적으로 특정한 패턴에 맞는 문자만 골라 출력하는 함수입니다.


이번에 우리에게 필요한 함수는 특정한 패턴이 문자열에 들어있는지 아닌지 판별하는 함수가 될 겁니다.


그런 함수가 바로 str_detect()입니다.


str_detect()는 어떤 문자열 안에 해당 패턴이 있으면 TRUE, 아니면 FALSE를 출력하는 구실을 합니다.


그럼 본격적으로 문제 풀이에 도전해 보겠습니다.


늘 그렇듯 제일 먼저 할 일은 tidyverse 패키지를 (설치하고) 불러오기.

#install.packages('tidyverse')
library('tidyverse')
## -- Attaching packages ------------------------- tidyverse 1.3.0 --
## √ ggplot2 3.3.0     √ purrr   0.3.3
## √ tibble  2.1.3     √ dplyr   0.8.4
## √ tidyr   1.0.2     √ stringr 1.4.0
## √ readr   1.3.1     √ forcats 0.5.0
## -- Conflicts ---------------------------- tidyverse_conflicts() --
## x dplyr::filter() masks stats::filter()
## x dplyr::lag()    masks stats::lag()


그럼 먼저 str_match() 함수를 쓰면 어떤 결과가 나오는지 확인해 볼까요?


아래는 'abced'라는 문자열에서 'abc' 패턴을 찾아보라는 코드입니다.

str_match('abcde', 'abc')
##      [,1] 
## [1,] "abc"


앞서 말씀드렸던 것처럼 그냥 'abc' 자체를 출력합니다.


이어서 이번에는 str_detect() 함수를 써서 'abc' 패턴을 찾아보는 코드를 써보겠습니다.

str_detect('abcde', 'abc')
## [1] TRUE


앞서 말씀드렸던 것처럼 이번에는 TRUE를 출력했습니다.


계속해서 'def'를 찾아볼까요?


'abced'에서는 'def'를 찾을 수 없기 때문에 FALSE가 나와야 합니다.  

str_detect('abcde', 'def')
## [1] FALSE


예, 정말 그렇게 나왔습니다.


혹시 abced에서 abc가 맨 앞에 있어서 TRUE가 나왔던 건 아닐까요?


이번에는 한번 'bcd'를 찾아보겠습니다.

str_detect('abcde', 'bcd')
## [1] TRUE


이번에도 잘 나왔습니다.


str_detect() 함수는 확실히 위치에 관계없이 패턴 존재 유무를 출력합니다.


이제 질문자께서 말씀하신 상황에 맞는 예제 데이터 프레임을 하나 만들겠습니다.

tribble(
  ~x1,
  'abc',
  'def',
  'ghi'
)
## # A tibble: 3 x 1
##   x1   
##   <chr>
## 1 abc  
## 2 def  
## 3 ghi


여기에 검출 결과를 출력하는 x3열을 추가해야겠죠?


tidyverse 생태계에서 열을 추가할 때 쓰는 함수는 mutate()입니다.


위에 있는 문장이 무슨  뜻인지 모르시겠다는 분이 계시면 '최대한 친절하게 쓴 R로 데이터 뽑아내기(feat. dplyr)' 포스트가 도움이 될 수 있습니다. 


일단 x1 열에 'abc'가 들어있는지 확인하는 코드를 써보겠습니다.

tribble(
  ~x1,
  'abc',
  'def',
  'ghi'
) %>%
  mutate(x3=str_detect(x1, 'abc'))
## # A tibble: 3 x 2
##   x1    x3   
##   <chr> <lgl>
## 1 abc   TRUE 
## 2 def   FALSE
## 3 ghi   FALSE


일단 합격입니다.


만약 패턴을 하나만 찾는 게 목표라면 이미 '미션 석세스'입니다.


그런데 질문자께서는 두 가지 조건을 원하셨습니다.


'abc'가 있으면 'yes', 'def'가 있으면 'no'를 출력해야 합니다.


이럴 때 생각할 수 있는 첫 번째 방법은 ifelse() 함수를 활용하는 것.


ifelse() 함수는 마이크로소프트(MS) 엑셀에서 if() 함수와 같은 기능을 한다고 생각하시면 편합니다.


그러니까 if(조건, 충족할 때, 아닐 때) 형태로 코드를 쓰시면 된다는 뜻입니다.


이번에는 조건이 두 개니까 ifelse(첫 번째 조건, 충족할 때, ifelse(두 번째 조건, 충족할 때, 아닐 때))처럼 쓰시면 되겠죠?


여기서 첫 번째 조건을 충족하려면 str_detect(x1, 'abc')가 참(TRUE)이어야 합니다.


그리고 참일 때 출력할 값은 'yes'입니다.


이 내용을 코드로 정리하면 ifelse(str_detect(x1, 'abc')==TRUE, 'yes', 아닐 때)처럼 쓸 수 있습니다.


R에서는 같다고 표시할 때 '='를 두 번 쓴다는 것만 알고 계시다면 어려울 게 없습니다.


여기서 뒤에 있는 '아닐 때'에는 'def'를 기준으로 같은 코드를 쓰면 됩니다.


그러면 ifelse(str_detect(x1, 'def'==TRUE, 'no', NA)로 쓸 수 있습니다.


def가 있으면 'no'를 출력하고 그것도 아닐 때는 '해당사항 없음'(NA)을 출력하라는 뜻입니다.  


이를 합치면 아래 같은 코드가 됩니다.

tribble(
  ~x1,
  'abc',
  'def',
  'ghi'
) %>%
  mutate(x3=ifelse(str_detect(x1, 'abc')==TRUE, 'yes',
                   ifelse(str_detect(x1, 'def')==TRUE, 'no', NA)))
## # A tibble: 3 x 2
##   x1    x3   
##   <chr> <chr>
## 1 abc   yes  
## 2 def   no   
## 3 ghi   <NA>


잘 나왔습니다. 이제 진짜 '미션 클리어'입니다.


여기서 한 걸음 더 나아가면 case_when()이라는 함수도 활용할 수 있습니다.


이 함수는 이번처럼 if(else)() 함수를 여러 번 써야 할 때 이를 줄여주는 구실을 합니다.


이런 케이스일 때는 이렇게, 저런 케이스일 때는 저렇게 해달라고 코드를 요약하는 방식입니다.


case_when()에서 조건과 결과 사이는 ~로 연결합니다.


이미 우리는 조건은 잘 알고 있으니 코드로 바로 쓰면 이렇게 됩니다.

tribble(
  ~x1,
  'abc',
  'def',
  'ghi'
) %>%
  mutate(x3=case_when(
    str_detect(x1, 'abc')~'yes',
    str_detect(x1, 'def')~'no'
  ))
## # A tibble: 3 x 2
##   x1    x3   
##   <chr> <chr>
## 1 abc   yes  
## 2 def   no   
## 3 ghi   <NA>


깔끔하게 잘 떨어졌습니다.


이번에는 추가 조건이 없어서 쓰지 않았는데 원래는 끝에 TRUE~'값' 형태로 예외를 지정해 줘야 할 때도 있습니다.


예컨대 이 경기에는 맨 끝에 'TRUE~NA_character_'까지 쓰는 게 원칙입니다.


여기서 알 수 있는 건 NA를 지정할 때도 데이터 유형까지 정해줘야 한다는 점입니다.


혹시 이해가 잘 가지 않으시거나 잘못된 내용이 있다면 언제든 알려주세요.


그럼 모두들 Happy Tidyversing!


댓글, 2

  • 댓글 수정/삭제 스탠리
    2020.11.10 18:13

    안녕하세요 기자님, 타이디버스에 이제 막 입문한 사람으로 기자님 블로그에서 정말 큰 도움을 얻고 있습니다.
    다름이 아니라 리스트 안에 들어있는 문자열과 tibble의 문자열을 비교하는 법을 문의드리려고 합니다. 예를 들자면 아래와 같습니다.

    t1 <- tribble(~team, 'tigers', 'lions', 'wyverns', 'bears')
    l1 <- list('eagles', 'tigers', 'wyverns')

    l1에 들어있는 문자열을 t1의 문자열과 비교해서 일치하는 경우에는 새로운 변수를 생성하여 t1 문자열 각각에 해당하는 새로운 문자열을 부여해주고 싶습니다. 제가 원하는 결과물의 예를 들어본다면 아래의 코드를 실행한 결과가 되겠지요.

    tribble( ~team, ~company, 'tigers', 'kia', 'lions', '', 'wyverns', 'sk', 'bears', '')

    모쪼록 기자님의 조언 부탁드립니다. 추운 날씨에 감기 조심하세요~



    •  수정/삭제 kini
      2020.11.13 08:29 신고

      여기서 리스트를 쓰시는 이유는 모르겠지만 리스트는 unlist() 함수를 거치면 티블로 변환할 수 있습니다.

      l1 %>%
      unlist() %>%
      as_tibble() -> l2

      l2 %>%
      mutate(company = c('hanhwa', 'kia', 'sk')) -> l2

      t1 %>%
      left_join(l2, by=c('team'='value'))

      이렇게 쓰시면 원하시는 결과를 얻으실 수 있을 겁니다.

account_circle
vpn_key
web

security

mode_edit
KindeR | 카테고리 다른 글 더 보기