분석하고자 하는 텍스트의 의미와 숨은 구조를 가장 잘 보여주는 특징, 성격, 또는 차원이 무엇인지 결정하는 작업
예: 어휘, 동의어, 등장 인물과 관계도, 의미 구조
앞으로 우리가 다루게 될 주제는 대부분 “특징(feature) 추출”과 “지식 공학,” 즉 추출된 정보로부터 지식체계를 구축하는 일
이를 위해서는 숨어있는 언어 구조를 연산 가능한 방식 표시해아 함.
그리고 연산 가능한 방식으로 표시하는 것은 언어를 단위화해야 함, 즉 Tokenization
예: 고유 어휘, 일상적 구문, 문법 단위 등
이러한 작업은 모두 텍스트 자료, 즉 코퍼스에서부터 시작한다.
코퍼스는 자연어를 포함하고 있는 관련 문서들의 집합을 말함. 크기는 다양할 수 있음. 중요한 것은, “관련성”
코퍼스는 주석 처리될 수 있음. 주석(Annotation) 처리란 텍스트 또는 문서의 “특징”을 덧붙이는 작업
문서는 텍스트 및 관련 정보를 포함하는 기본 단위 자료: 예: 신문기사, 트윗, 리뷰
문서는 문단(Paragraph)으로 쪼개질 수 있음. 각 문단은 문서를 구성하는 각각의 아이디어를 표현.
문단은 문장(Sentence)으로 쪼개질 수 있음. 각 문장은 문법 구조를 가지고 있음. 즉, 완결성있는 언어적 표현.
문장은 어휘(Word) 및 문장부호(Punctuation)로 구성되어 있음. 어휘와 부호는 각각의 의미를 내포하고 있지만, 그것들이 함께 사용되며 보다 풍부하고 복잡한 의미를 표현.
마지막으로 어휘는 음절(Syllable), 음소(Phoneme), 접사(Affix) 및 문자(Character)로 구성되고, 이 단위들은 어휘로 조합되어야지만 의미를 표현.
결국, 코퍼스는 어휘 -> 문장 -> 문단 -> 문서 -> 코퍼스라는 위계적 구조로 구성되어 있음.
언어학의 이론 검증이나 새로운 언어적 개념 및 이론 제시를 위해서 종종 포괄적 코퍼스(_예: Brown Corpus, Wikipedia Corpus, 또는 Cornell Movie Dialogue Corpus)를 이용.
하지만, 언어 사용 패턴과 그 의미는 분야에 따라 매우 다른 경향을 보임. 예: Credit이라는 어휘는 학교내에서와 은행에서 다른 의미로 사용됨. 따라서 어휘의 의미를 분야 특성에 맞게 학습해야 정확한 예측이 가능
특정 분야에 국한된 코퍼스는 언어 전반에 대한 개념 및 이론을 검증하거나 확장하는데는 비효과적이지만, 텍스트 분석을 통해 정확한(타당성있는) 예측 모델 구축에는 필수적.
특정 분야 코퍼스 구축은 웹스크래핑, API 활용, 또는 RSS 처리 등의 방법을 통해 수집된 자연어 형태의 텍스트를 연산 가능한 구조의 형태로 변환하는 작업(정형화)을 거치게 된다. 그리고, 이 작업은 꽤 복잡하고, 어렵고, 귀찮고, 시간이 오래 걸린다!!!
텍스트 자료의 수집 및 정형화 과정에는 몇가지 고려해야 될 사항들이 있다.
코퍼스 크기: 연구의 목적 및 기술적 제한사항 고려
데이터 가공: 수집된 텍스트 자료의 정제와 처리를 통한 구조화 필요 정도 고려
대부분의 가공 전 텍스트 자료는 HTML, JSON, CSV 형태로 제공
NoSQL은 대용량의 관계형 데이터베이스 저장, 관리 및 처리에 유용한 오픈 소스 시스템 (특히, 비정형 데이터 처리에 탁월)
하지만, 특정 도메인 코퍼스의 경우 크기가 그리 크지 않기 때문에 파일 형태로 저장하는 것도 용이성 측면에서 고려할만 함.
코퍼스를 처리 후 분석에 활용하기 위해서는 데이터 구조화하는 것이 필요.
이 때, JSON 데이터 구조가 유용함. 각 문서의 텍스트를 구분해서 저장하고 있을 뿐만 아니라, 문서의 메타정보(Metadata; 저자명, 시간, 장소 등)를 포함시킬 수 있음.
코퍼스 구조는 각 문서의 텍스트(내용)는 물론 문서의 메타정보 및 특징(feature)에 기초해 데이터를 재구성할 수 있는 형태로 조직화하는 것이 중요함.
library("streamR")
## Loading required package: RCurl
## Loading required package: bitops
## Loading required package: rjson
## Loading required package: ndjson
data("example_tweets")
example_tweets
## [1] "{\"created_at\":\"Tue Dec 11 22:51:36 +0000 2012\",\"id\":278633157983604736,\"id_str\":\"278633157983604736\",\"text\":\"We are updating the Site Streams cluster. Site Streams consumers may have some issues with control streams for 30~60 minutes. ^ARK\",\"source\":\"web\",\"truncated\":false,\"in_reply_to_status_id\":null,\"in_reply_to_status_id_str\":null,\"in_reply_to_user_id\":null,\"in_reply_to_user_id_str\":null,\"in_reply_to_screen_name\":null,\"user\":{\"id\":6253282,\"id_str\":\"6253282\",\"name\":\"Twitter API\",\"screen_name\":\"twitterapi\",\"location\":\"San Francisco, CA\",\"url\":\"http:\\/\\/dev.twitter.com\",\"description\":\"The Real Twitter API. I tweet about API changes, service issues and happily answer questions about Twitter and our API. Don't get an answer? It's on my website.\",\"protected\":false,\"followers_count\":1432852,\"friends_count\":33,\"listed_count\":11189,\"created_at\":\"Wed May 23 06:01:13 +0000 2007\",\"favourites_count\":25,\"utc_offset\":-28800,\"time_zone\":\"Pacific Time (US & Canada)\",\"geo_enabled\":true,\"verified\":true,\"statuses_count\":3363,\"lang\":\"en\",\"contributors_enabled\":true,\"is_translator\":false,\"profile_background_color\":\"C0DEED\",\"profile_background_image_url\":\"http:\\/\\/a0.twimg.com\\/profile_background_images\\/656927849\\/miyt9dpjz77sc0w3d4vj.png\",\"profile_background_image_url_https\":\"https:\\/\\/si0.twimg.com\\/profile_background_images\\/656927849\\/miyt9dpjz77sc0w3d4vj.png\",\"profile_background_tile\":true,\"profile_image_url\":\"http:\\/\\/a0.twimg.com\\/profile_images\\/2284174872\\/7df3h38zabcvjylnyfe3_normal.png\",\"profile_image_url_https\":\"https:\\/\\/si0.twimg.com\\/profile_images\\/2284174872\\/7df3h38zabcvjylnyfe3_normal.png\",\"profile_banner_url\":\"https:\\/\\/si0.twimg.com\\/profile_banners\\/6253282\\/1347394302\",\"profile_link_color\":\"0084B4\",\"profile_sidebar_border_color\":\"C0DEED\",\"profile_sidebar_fill_color\":\"DDEEF6\",\"profile_text_color\":\"333333\",\"profile_use_background_image\":true,\"default_profile\":false,\"default_profile_image\":false,\"following\":null,\"follow_request_sent\":null,\"notifications\":null},\"geo\":null,\"coordinates\":null,\"place\":null,\"contributors\":[7588892],\"retweet_count\":114,\"favorited\":false,\"retweeted\":false}"
## [2] "{\"created_at\":\"Thu Dec 06 18:31:56 +0000 2012\",\"id\":276755874402414593,\"id_str\":\"276755874402414593\",\"text\":\"Important - t.co will soon be changing in length: https:\\/\\/t.co\\/nqvYxZAk ^JC\",\"source\":\"web\",\"truncated\":false,\"in_reply_to_status_id\":null,\"in_reply_to_status_id_str\":null,\"in_reply_to_user_id\":null,\"in_reply_to_user_id_str\":null,\"in_reply_to_screen_name\":null,\"user\":{\"id\":6253282,\"id_str\":\"6253282\",\"name\":\"Twitter API\",\"screen_name\":\"twitterapi\",\"location\":\"San Francisco, CA\",\"url\":\"http:\\/\\/dev.twitter.com\",\"description\":\"The Real Twitter API. I tweet about API changes, service issues and happily answer questions about Twitter and our API. Don't get an answer? It's on my website.\",\"protected\":false,\"followers_count\":1432852,\"friends_count\":33,\"listed_count\":11189,\"created_at\":\"Wed May 23 06:01:13 +0000 2007\",\"favourites_count\":25,\"utc_offset\":-28800,\"time_zone\":\"Pacific Time (US & Canada)\",\"geo_enabled\":true,\"verified\":true,\"statuses_count\":3363,\"lang\":\"en\",\"contributors_enabled\":true,\"is_translator\":false,\"profile_background_color\":\"C0DEED\",\"profile_background_image_url\":\"http:\\/\\/a0.twimg.com\\/profile_background_images\\/656927849\\/miyt9dpjz77sc0w3d4vj.png\",\"profile_background_image_url_https\":\"https:\\/\\/si0.twimg.com\\/profile_background_images\\/656927849\\/miyt9dpjz77sc0w3d4vj.png\",\"profile_background_tile\":true,\"profile_image_url\":\"http:\\/\\/a0.twimg.com\\/profile_images\\/2284174872\\/7df3h38zabcvjylnyfe3_normal.png\",\"profile_image_url_https\":\"https:\\/\\/si0.twimg.com\\/profile_images\\/2284174872\\/7df3h38zabcvjylnyfe3_normal.png\",\"profile_banner_url\":\"https:\\/\\/si0.twimg.com\\/profile_banners\\/6253282\\/1347394302\",\"profile_link_color\":\"0084B4\",\"profile_sidebar_border_color\":\"C0DEED\",\"profile_sidebar_fill_color\":\"DDEEF6\",\"profile_text_color\":\"333333\",\"profile_use_background_image\":true,\"default_profile\":false,\"default_profile_image\":false,\"following\":null,\"follow_request_sent\":null,\"notifications\":null},\"geo\":null,\"coordinates\":null,\"place\":null,\"contributors\":[14927800],\"retweet_count\":120,\"favorited\":false,\"retweeted\":false,\"possibly_sensitive\":false}"
## [3] "{\"created_at\":\"Wed Dec 05 19:51:52 +0000 2012\",\"id\":276413601651245056,\"id_str\":\"276413601651245056\",\"text\":\"\\\"@urbandictionary grows daily followers 6.5x by implementing the Follow Button\\\" - https:\\/\\/t.co\\/oisy5Rlu ^JC\",\"source\":\"web\",\"truncated\":false,\"in_reply_to_status_id\":null,\"in_reply_to_status_id_str\":null,\"in_reply_to_user_id\":null,\"in_reply_to_user_id_str\":null,\"in_reply_to_screen_name\":null,\"user\":{\"id\":6253282,\"id_str\":\"6253282\",\"name\":\"Twitter API\",\"screen_name\":\"twitterapi\",\"location\":\"San Francisco, CA\",\"url\":\"http:\\/\\/dev.twitter.com\",\"description\":\"The Real Twitter API. I tweet about API changes, service issues and happily answer questions about Twitter and our API. Don't get an answer? It's on my website.\",\"protected\":false,\"followers_count\":1432852,\"friends_count\":33,\"listed_count\":11189,\"created_at\":\"Wed May 23 06:01:13 +0000 2007\",\"favourites_count\":25,\"utc_offset\":-28800,\"time_zone\":\"Pacific Time (US & Canada)\",\"geo_enabled\":true,\"verified\":true,\"statuses_count\":3363,\"lang\":\"en\",\"contributors_enabled\":true,\"is_translator\":false,\"profile_background_color\":\"C0DEED\",\"profile_background_image_url\":\"http:\\/\\/a0.twimg.com\\/profile_background_images\\/656927849\\/miyt9dpjz77sc0w3d4vj.png\",\"profile_background_image_url_https\":\"https:\\/\\/si0.twimg.com\\/profile_background_images\\/656927849\\/miyt9dpjz77sc0w3d4vj.png\",\"profile_background_tile\":true,\"profile_image_url\":\"http:\\/\\/a0.twimg.com\\/profile_images\\/2284174872\\/7df3h38zabcvjylnyfe3_normal.png\",\"profile_image_url_https\":\"https:\\/\\/si0.twimg.com\\/profile_images\\/2284174872\\/7df3h38zabcvjylnyfe3_normal.png\",\"profile_banner_url\":\"https:\\/\\/si0.twimg.com\\/profile_banners\\/6253282\\/1347394302\",\"profile_link_color\":\"0084B4\",\"profile_sidebar_border_color\":\"C0DEED\",\"profile_sidebar_fill_color\":\"DDEEF6\",\"profile_text_color\":\"333333\",\"profile_use_background_image\":true,\"default_profile\":false,\"default_profile_image\":false,\"following\":null,\"follow_request_sent\":null,\"notifications\":null},\"geo\":null,\"coordinates\":null,\"place\":null,\"contributors\":[14927800],\"retweet_count\":167,\"favorited\":false,\"retweeted\":false,\"possibly_sensitive\":false}"
## [4] "{\"created_at\":\"Mon Dec 03 21:53:08 +0000 2012\",\"id\":275719344317685760,\"id_str\":\"275719344317685760\",\"text\":\"We now have a page which tracks upcoming and recent changes to the platform: https:\\/\\/t.co\\/3gMjdnBp ^ARK\",\"source\":\"web\",\"truncated\":false,\"in_reply_to_status_id\":null,\"in_reply_to_status_id_str\":null,\"in_reply_to_user_id\":null,\"in_reply_to_user_id_str\":null,\"in_reply_to_screen_name\":null,\"user\":{\"id\":6253282,\"id_str\":\"6253282\",\"name\":\"Twitter API\",\"screen_name\":\"twitterapi\",\"location\":\"San Francisco, CA\",\"url\":\"http:\\/\\/dev.twitter.com\",\"description\":\"The Real Twitter API. I tweet about API changes, service issues and happily answer questions about Twitter and our API. Don't get an answer? It's on my website.\",\"protected\":false,\"followers_count\":1432852,\"friends_count\":33,\"listed_count\":11189,\"created_at\":\"Wed May 23 06:01:13 +0000 2007\",\"favourites_count\":25,\"utc_offset\":-28800,\"time_zone\":\"Pacific Time (US & Canada)\",\"geo_enabled\":true,\"verified\":true,\"statuses_count\":3363,\"lang\":\"en\",\"contributors_enabled\":true,\"is_translator\":false,\"profile_background_color\":\"C0DEED\",\"profile_background_image_url\":\"http:\\/\\/a0.twimg.com\\/profile_background_images\\/656927849\\/miyt9dpjz77sc0w3d4vj.png\",\"profile_background_image_url_https\":\"https:\\/\\/si0.twimg.com\\/profile_background_images\\/656927849\\/miyt9dpjz77sc0w3d4vj.png\",\"profile_background_tile\":true,\"profile_image_url\":\"http:\\/\\/a0.twimg.com\\/profile_images\\/2284174872\\/7df3h38zabcvjylnyfe3_normal.png\",\"profile_image_url_https\":\"https:\\/\\/si0.twimg.com\\/profile_images\\/2284174872\\/7df3h38zabcvjylnyfe3_normal.png\",\"profile_banner_url\":\"https:\\/\\/si0.twimg.com\\/profile_banners\\/6253282\\/1347394302\",\"profile_link_color\":\"0084B4\",\"profile_sidebar_border_color\":\"C0DEED\",\"profile_sidebar_fill_color\":\"DDEEF6\",\"profile_text_color\":\"333333\",\"profile_use_background_image\":true,\"default_profile\":false,\"default_profile_image\":false,\"following\":null,\"follow_request_sent\":null,\"notifications\":null},\"geo\":null,\"coordinates\":null,\"place\":null,\"contributors\":[7588892],\"retweet_count\":50,\"favorited\":false,\"retweeted\":false,\"possibly_sensitive\":false}"
## [5] "{\"created_at\":\"Mon Dec 03 20:25:22 +0000 2012\",\"id\":275697256529788929,\"id_str\":\"275697256529788929\",\"text\":\"We're extending the deprecation period for @Anywhere to align with API v1's retirement in March 2013. https:\\/\\/t.co\\/ybnvlErk ^TS\",\"source\":\"web\",\"truncated\":false,\"in_reply_to_status_id\":null,\"in_reply_to_status_id_str\":null,\"in_reply_to_user_id\":null,\"in_reply_to_user_id_str\":null,\"in_reply_to_screen_name\":null,\"user\":{\"id\":6253282,\"id_str\":\"6253282\",\"name\":\"Twitter API\",\"screen_name\":\"twitterapi\",\"location\":\"San Francisco, CA\",\"url\":\"http:\\/\\/dev.twitter.com\",\"description\":\"The Real Twitter API. I tweet about API changes, service issues and happily answer questions about Twitter and our API. Don't get an answer? It's on my website.\",\"protected\":false,\"followers_count\":1432852,\"friends_count\":33,\"listed_count\":11189,\"created_at\":\"Wed May 23 06:01:13 +0000 2007\",\"favourites_count\":25,\"utc_offset\":-28800,\"time_zone\":\"Pacific Time (US & Canada)\",\"geo_enabled\":true,\"verified\":true,\"statuses_count\":3363,\"lang\":\"en\",\"contributors_enabled\":true,\"is_translator\":false,\"profile_background_color\":\"C0DEED\",\"profile_background_image_url\":\"http:\\/\\/a0.twimg.com\\/profile_background_images\\/656927849\\/miyt9dpjz77sc0w3d4vj.png\",\"profile_background_image_url_https\":\"https:\\/\\/si0.twimg.com\\/profile_background_images\\/656927849\\/miyt9dpjz77sc0w3d4vj.png\",\"profile_background_tile\":true,\"profile_image_url\":\"http:\\/\\/a0.twimg.com\\/profile_images\\/2284174872\\/7df3h38zabcvjylnyfe3_normal.png\",\"profile_image_url_https\":\"https:\\/\\/si0.twimg.com\\/profile_images\\/2284174872\\/7df3h38zabcvjylnyfe3_normal.png\",\"profile_banner_url\":\"https:\\/\\/si0.twimg.com\\/profile_banners\\/6253282\\/1347394302\",\"profile_link_color\":\"0084B4\",\"profile_sidebar_border_color\":\"C0DEED\",\"profile_sidebar_fill_color\":\"DDEEF6\",\"profile_text_color\":\"333333\",\"profile_use_background_image\":true,\"default_profile\":false,\"default_profile_image\":false,\"following\":null,\"follow_request_sent\":null,\"notifications\":null},\"geo\":null,\"coordinates\":null,\"place\":null,\"contributors\":[819797],\"retweet_count\":47,\"favorited\":false,\"retweeted\":false,\"possibly_sensitive\":false}"
## [6] "{\"created_at\":\"Thu Nov 29 20:05:38 +0000 2012\",\"id\":274242737367310336,\"id_str\":\"274242737367310336\",\"text\":\"Two new methods in API v1.1 provide simplified access to user friend & follower data: https:\\/\\/t.co\\/vPja8oUs https:\\/\\/t.co\\/AKvLCan2 ^TS\",\"source\":\"web\",\"truncated\":false,\"in_reply_to_status_id\":null,\"in_reply_to_status_id_str\":null,\"in_reply_to_user_id\":null,\"in_reply_to_user_id_str\":null,\"in_reply_to_screen_name\":null,\"user\":{\"id\":6253282,\"id_str\":\"6253282\",\"name\":\"Twitter API\",\"screen_name\":\"twitterapi\",\"location\":\"San Francisco, CA\",\"url\":\"http:\\/\\/dev.twitter.com\",\"description\":\"The Real Twitter API. I tweet about API changes, service issues and happily answer questions about Twitter and our API. Don't get an answer? It's on my website.\",\"protected\":false,\"followers_count\":1432852,\"friends_count\":33,\"listed_count\":11189,\"created_at\":\"Wed May 23 06:01:13 +0000 2007\",\"favourites_count\":25,\"utc_offset\":-28800,\"time_zone\":\"Pacific Time (US & Canada)\",\"geo_enabled\":true,\"verified\":true,\"statuses_count\":3363,\"lang\":\"en\",\"contributors_enabled\":true,\"is_translator\":false,\"profile_background_color\":\"C0DEED\",\"profile_background_image_url\":\"http:\\/\\/a0.twimg.com\\/profile_background_images\\/656927849\\/miyt9dpjz77sc0w3d4vj.png\",\"profile_background_image_url_https\":\"https:\\/\\/si0.twimg.com\\/profile_background_images\\/656927849\\/miyt9dpjz77sc0w3d4vj.png\",\"profile_background_tile\":true,\"profile_image_url\":\"http:\\/\\/a0.twimg.com\\/profile_images\\/2284174872\\/7df3h38zabcvjylnyfe3_normal.png\",\"profile_image_url_https\":\"https:\\/\\/si0.twimg.com\\/profile_images\\/2284174872\\/7df3h38zabcvjylnyfe3_normal.png\",\"profile_banner_url\":\"https:\\/\\/si0.twimg.com\\/profile_banners\\/6253282\\/1347394302\",\"profile_link_color\":\"0084B4\",\"profile_sidebar_border_color\":\"C0DEED\",\"profile_sidebar_fill_color\":\"DDEEF6\",\"profile_text_color\":\"333333\",\"profile_use_background_image\":true,\"default_profile\":false,\"default_profile_image\":false,\"following\":null,\"follow_request_sent\":null,\"notifications\":null},\"geo\":null,\"coordinates\":null,\"place\":null,\"contributors\":[819797],\"retweet_count\":65,\"favorited\":false,\"retweeted\":false,\"possibly_sensitive\":false}"
## [7] "{\"created_at\":\"Mon Nov 26 18:41:08 +0000 2012\",\"id\":273134307248332800,\"id_str\":\"273134307248332800\",\"text\":\"Please use the new #TwitterCards thread for help with your implementation - Twitter Cards: I have a problem, help me! https:\\/\\/t.co\\/jvJPyE3I\",\"source\":\"web\",\"truncated\":false,\"in_reply_to_status_id\":null,\"in_reply_to_status_id_str\":null,\"in_reply_to_user_id\":null,\"in_reply_to_user_id_str\":null,\"in_reply_to_screen_name\":null,\"user\":{\"id\":6253282,\"id_str\":\"6253282\",\"name\":\"Twitter API\",\"screen_name\":\"twitterapi\",\"location\":\"San Francisco, CA\",\"url\":\"http:\\/\\/dev.twitter.com\",\"description\":\"The Real Twitter API. I tweet about API changes, service issues and happily answer questions about Twitter and our API. Don't get an answer? It's on my website.\",\"protected\":false,\"followers_count\":1432852,\"friends_count\":33,\"listed_count\":11189,\"created_at\":\"Wed May 23 06:01:13 +0000 2007\",\"favourites_count\":25,\"utc_offset\":-28800,\"time_zone\":\"Pacific Time (US & Canada)\",\"geo_enabled\":true,\"verified\":true,\"statuses_count\":3363,\"lang\":\"en\",\"contributors_enabled\":true,\"is_translator\":false,\"profile_background_color\":\"C0DEED\",\"profile_background_image_url\":\"http:\\/\\/a0.twimg.com\\/profile_background_images\\/656927849\\/miyt9dpjz77sc0w3d4vj.png\",\"profile_background_image_url_https\":\"https:\\/\\/si0.twimg.com\\/profile_background_images\\/656927849\\/miyt9dpjz77sc0w3d4vj.png\",\"profile_background_tile\":true,\"profile_image_url\":\"http:\\/\\/a0.twimg.com\\/profile_images\\/2284174872\\/7df3h38zabcvjylnyfe3_normal.png\",\"profile_image_url_https\":\"https:\\/\\/si0.twimg.com\\/profile_images\\/2284174872\\/7df3h38zabcvjylnyfe3_normal.png\",\"profile_banner_url\":\"https:\\/\\/si0.twimg.com\\/profile_banners\\/6253282\\/1347394302\",\"profile_link_color\":\"0084B4\",\"profile_sidebar_border_color\":\"C0DEED\",\"profile_sidebar_fill_color\":\"DDEEF6\",\"profile_text_color\":\"333333\",\"profile_use_background_image\":true,\"default_profile\":false,\"default_profile_image\":false,\"following\":null,\"follow_request_sent\":null,\"notifications\":null},\"geo\":null,\"coordinates\":null,\"place\":null,\"contributors\":[657693],\"retweet_count\":52,\"favorited\":false,\"retweeted\":false,\"possibly_sensitive\":false}"
## [8] "{\"created_at\":\"Fri Nov 16 00:07:48 +0000 2012\",\"id\":269230249009618944,\"id_str\":\"269230249009618944\",\"text\":\"The unexpected HTTP 400s you may have noticed while authenticating with the API should be resolving now. ^TS\",\"source\":\"web\",\"truncated\":false,\"in_reply_to_status_id\":null,\"in_reply_to_status_id_str\":null,\"in_reply_to_user_id\":null,\"in_reply_to_user_id_str\":null,\"in_reply_to_screen_name\":null,\"user\":{\"id\":6253282,\"id_str\":\"6253282\",\"name\":\"Twitter API\",\"screen_name\":\"twitterapi\",\"location\":\"San Francisco, CA\",\"url\":\"http:\\/\\/dev.twitter.com\",\"description\":\"The Real Twitter API. I tweet about API changes, service issues and happily answer questions about Twitter and our API. Don't get an answer? It's on my website.\",\"protected\":false,\"followers_count\":1432852,\"friends_count\":33,\"listed_count\":11189,\"created_at\":\"Wed May 23 06:01:13 +0000 2007\",\"favourites_count\":25,\"utc_offset\":-28800,\"time_zone\":\"Pacific Time (US & Canada)\",\"geo_enabled\":true,\"verified\":true,\"statuses_count\":3363,\"lang\":\"en\",\"contributors_enabled\":true,\"is_translator\":false,\"profile_background_color\":\"C0DEED\",\"profile_background_image_url\":\"http:\\/\\/a0.twimg.com\\/profile_background_images\\/656927849\\/miyt9dpjz77sc0w3d4vj.png\",\"profile_background_image_url_https\":\"https:\\/\\/si0.twimg.com\\/profile_background_images\\/656927849\\/miyt9dpjz77sc0w3d4vj.png\",\"profile_background_tile\":true,\"profile_image_url\":\"http:\\/\\/a0.twimg.com\\/profile_images\\/2284174872\\/7df3h38zabcvjylnyfe3_normal.png\",\"profile_image_url_https\":\"https:\\/\\/si0.twimg.com\\/profile_images\\/2284174872\\/7df3h38zabcvjylnyfe3_normal.png\",\"profile_banner_url\":\"https:\\/\\/si0.twimg.com\\/profile_banners\\/6253282\\/1347394302\",\"profile_link_color\":\"0084B4\",\"profile_sidebar_border_color\":\"C0DEED\",\"profile_sidebar_fill_color\":\"DDEEF6\",\"profile_text_color\":\"333333\",\"profile_use_background_image\":true,\"default_profile\":false,\"default_profile_image\":false,\"following\":null,\"follow_request_sent\":null,\"notifications\":null},\"geo\":null,\"coordinates\":null,\"place\":null,\"contributors\":[819797],\"retweet_count\":85,\"favorited\":false,\"retweeted\":false}"
## [9] "{\"created_at\":\"Fri Nov 09 23:20:43 +0000 2012\",\"id\":267044073968390144,\"id_str\":\"267044073968390144\",\"text\":\"The ability to upload, remove, and retrieve profile banner images is now available for API v1.1. https:\\/\\/t.co\\/MtuaToSo ^TS\",\"source\":\"web\",\"truncated\":false,\"in_reply_to_status_id\":null,\"in_reply_to_status_id_str\":null,\"in_reply_to_user_id\":null,\"in_reply_to_user_id_str\":null,\"in_reply_to_screen_name\":null,\"user\":{\"id\":6253282,\"id_str\":\"6253282\",\"name\":\"Twitter API\",\"screen_name\":\"twitterapi\",\"location\":\"San Francisco, CA\",\"url\":\"http:\\/\\/dev.twitter.com\",\"description\":\"The Real Twitter API. I tweet about API changes, service issues and happily answer questions about Twitter and our API. Don't get an answer? It's on my website.\",\"protected\":false,\"followers_count\":1432852,\"friends_count\":33,\"listed_count\":11189,\"created_at\":\"Wed May 23 06:01:13 +0000 2007\",\"favourites_count\":25,\"utc_offset\":-28800,\"time_zone\":\"Pacific Time (US & Canada)\",\"geo_enabled\":true,\"verified\":true,\"statuses_count\":3363,\"lang\":\"en\",\"contributors_enabled\":true,\"is_translator\":false,\"profile_background_color\":\"C0DEED\",\"profile_background_image_url\":\"http:\\/\\/a0.twimg.com\\/profile_background_images\\/656927849\\/miyt9dpjz77sc0w3d4vj.png\",\"profile_background_image_url_https\":\"https:\\/\\/si0.twimg.com\\/profile_background_images\\/656927849\\/miyt9dpjz77sc0w3d4vj.png\",\"profile_background_tile\":true,\"profile_image_url\":\"http:\\/\\/a0.twimg.com\\/profile_images\\/2284174872\\/7df3h38zabcvjylnyfe3_normal.png\",\"profile_image_url_https\":\"https:\\/\\/si0.twimg.com\\/profile_images\\/2284174872\\/7df3h38zabcvjylnyfe3_normal.png\",\"profile_banner_url\":\"https:\\/\\/si0.twimg.com\\/profile_banners\\/6253282\\/1347394302\",\"profile_link_color\":\"0084B4\",\"profile_sidebar_border_color\":\"C0DEED\",\"profile_sidebar_fill_color\":\"DDEEF6\",\"profile_text_color\":\"333333\",\"profile_use_background_image\":true,\"default_profile\":false,\"default_profile_image\":false,\"following\":null,\"follow_request_sent\":null,\"notifications\":null},\"geo\":null,\"coordinates\":null,\"place\":null,\"contributors\":[819797],\"retweet_count\":60,\"favorited\":false,\"retweeted\":false,\"possibly_sensitive\":false}"
## [10] "{\"created_at\":\"Thu Nov 08 19:44:07 +0000 2012\",\"id\":266627178429575168,\"id_str\":\"266627178429575168\",\"text\":\"The issue we reported earlier with Embeddable Timelines has been resolved. https:\\/\\/t.co\\/pDyHa1kX ^JC\",\"source\":\"web\",\"truncated\":false,\"in_reply_to_status_id\":null,\"in_reply_to_status_id_str\":null,\"in_reply_to_user_id\":null,\"in_reply_to_user_id_str\":null,\"in_reply_to_screen_name\":null,\"user\":{\"id\":6253282,\"id_str\":\"6253282\",\"name\":\"Twitter API\",\"screen_name\":\"twitterapi\",\"location\":\"San Francisco, CA\",\"url\":\"http:\\/\\/dev.twitter.com\",\"description\":\"The Real Twitter API. I tweet about API changes, service issues and happily answer questions about Twitter and our API. Don't get an answer? It's on my website.\",\"protected\":false,\"followers_count\":1432852,\"friends_count\":33,\"listed_count\":11189,\"created_at\":\"Wed May 23 06:01:13 +0000 2007\",\"favourites_count\":25,\"utc_offset\":-28800,\"time_zone\":\"Pacific Time (US & Canada)\",\"geo_enabled\":true,\"verified\":true,\"statuses_count\":3363,\"lang\":\"en\",\"contributors_enabled\":true,\"is_translator\":false,\"profile_background_color\":\"C0DEED\",\"profile_background_image_url\":\"http:\\/\\/a0.twimg.com\\/profile_background_images\\/656927849\\/miyt9dpjz77sc0w3d4vj.png\",\"profile_background_image_url_https\":\"https:\\/\\/si0.twimg.com\\/profile_background_images\\/656927849\\/miyt9dpjz77sc0w3d4vj.png\",\"profile_background_tile\":true,\"profile_image_url\":\"http:\\/\\/a0.twimg.com\\/profile_images\\/2284174872\\/7df3h38zabcvjylnyfe3_normal.png\",\"profile_image_url_https\":\"https:\\/\\/si0.twimg.com\\/profile_images\\/2284174872\\/7df3h38zabcvjylnyfe3_normal.png\",\"profile_banner_url\":\"https:\\/\\/si0.twimg.com\\/profile_banners\\/6253282\\/1347394302\",\"profile_link_color\":\"0084B4\",\"profile_sidebar_border_color\":\"C0DEED\",\"profile_sidebar_fill_color\":\"DDEEF6\",\"profile_text_color\":\"333333\",\"profile_use_background_image\":true,\"default_profile\":false,\"default_profile_image\":false,\"following\":null,\"follow_request_sent\":null,\"notifications\":null},\"geo\":null,\"coordinates\":null,\"place\":null,\"contributors\":[14927800],\"retweet_count\":29,\"favorited\":false,\"retweeted\":false,\"possibly_sensitive\":false}"
example_tweets[1]
## [1] "{\"created_at\":\"Tue Dec 11 22:51:36 +0000 2012\",\"id\":278633157983604736,\"id_str\":\"278633157983604736\",\"text\":\"We are updating the Site Streams cluster. Site Streams consumers may have some issues with control streams for 30~60 minutes. ^ARK\",\"source\":\"web\",\"truncated\":false,\"in_reply_to_status_id\":null,\"in_reply_to_status_id_str\":null,\"in_reply_to_user_id\":null,\"in_reply_to_user_id_str\":null,\"in_reply_to_screen_name\":null,\"user\":{\"id\":6253282,\"id_str\":\"6253282\",\"name\":\"Twitter API\",\"screen_name\":\"twitterapi\",\"location\":\"San Francisco, CA\",\"url\":\"http:\\/\\/dev.twitter.com\",\"description\":\"The Real Twitter API. I tweet about API changes, service issues and happily answer questions about Twitter and our API. Don't get an answer? It's on my website.\",\"protected\":false,\"followers_count\":1432852,\"friends_count\":33,\"listed_count\":11189,\"created_at\":\"Wed May 23 06:01:13 +0000 2007\",\"favourites_count\":25,\"utc_offset\":-28800,\"time_zone\":\"Pacific Time (US & Canada)\",\"geo_enabled\":true,\"verified\":true,\"statuses_count\":3363,\"lang\":\"en\",\"contributors_enabled\":true,\"is_translator\":false,\"profile_background_color\":\"C0DEED\",\"profile_background_image_url\":\"http:\\/\\/a0.twimg.com\\/profile_background_images\\/656927849\\/miyt9dpjz77sc0w3d4vj.png\",\"profile_background_image_url_https\":\"https:\\/\\/si0.twimg.com\\/profile_background_images\\/656927849\\/miyt9dpjz77sc0w3d4vj.png\",\"profile_background_tile\":true,\"profile_image_url\":\"http:\\/\\/a0.twimg.com\\/profile_images\\/2284174872\\/7df3h38zabcvjylnyfe3_normal.png\",\"profile_image_url_https\":\"https:\\/\\/si0.twimg.com\\/profile_images\\/2284174872\\/7df3h38zabcvjylnyfe3_normal.png\",\"profile_banner_url\":\"https:\\/\\/si0.twimg.com\\/profile_banners\\/6253282\\/1347394302\",\"profile_link_color\":\"0084B4\",\"profile_sidebar_border_color\":\"C0DEED\",\"profile_sidebar_fill_color\":\"DDEEF6\",\"profile_text_color\":\"333333\",\"profile_use_background_image\":true,\"default_profile\":false,\"default_profile_image\":false,\"following\":null,\"follow_request_sent\":null,\"notifications\":null},\"geo\":null,\"coordinates\":null,\"place\":null,\"contributors\":[7588892],\"retweet_count\":114,\"favorited\":false,\"retweeted\":false}"
코퍼스의 구조화 및 저장 작업 완료 후에는 코퍼스를 프로그래밍 작업 가능한 형태로 전환시켜야 함.
주의사항: 텍스트의 전처리와 토큰화 및 주석 작업은 컴퓨터 메모리 용량이 고려되어야 함
tweets <- parseTweets(example_tweets)
## 10 tweets have been parsed.
tweets
## text
## 1 We are updating the Site Streams cluster. Site Streams consumers may have some issues with control streams for 30~60 minutes. ^ARK
## 2 Important - t.co will soon be changing in length: https://t.co/nqvYxZAk ^JC
## 3 "@urbandictionary grows daily followers 6.5x by implementing the Follow Button" - https://t.co/oisy5Rlu ^JC
## 4 We now have a page which tracks upcoming and recent changes to the platform: https://t.co/3gMjdnBp ^ARK
## 5 We're extending the deprecation period for @Anywhere to align with API v1's retirement in March 2013. https://t.co/ybnvlErk ^TS
## 6 Two new methods in API v1.1 provide simplified access to user friend & follower data: https://t.co/vPja8oUs https://t.co/AKvLCan2 ^TS
## 7 Please use the new #TwitterCards thread for help with your implementation - Twitter Cards: I have a problem, help me! https://t.co/jvJPyE3I
## 8 The unexpected HTTP 400s you may have noticed while authenticating with the API should be resolving now. ^TS
## 9 The ability to upload, remove, and retrieve profile banner images is now available for API v1.1. https://t.co/MtuaToSo ^TS
## 10 The issue we reported earlier with Embeddable Timelines has been resolved. https://t.co/pDyHa1kX ^JC
## retweet_count favorite_count favorited truncated id_str
## 1 114 NA FALSE FALSE 278633157983604736
## 2 120 NA FALSE FALSE 276755874402414593
## 3 167 NA FALSE FALSE 276413601651245056
## 4 50 NA FALSE FALSE 275719344317685760
## 5 47 NA FALSE FALSE 275697256529788929
## 6 65 NA FALSE FALSE 274242737367310336
## 7 52 NA FALSE FALSE 273134307248332800
## 8 85 NA FALSE FALSE 269230249009618944
## 9 60 NA FALSE FALSE 267044073968390144
## 10 29 NA FALSE FALSE 266627178429575168
## in_reply_to_screen_name source retweeted created_at
## 1 NA web FALSE Tue Dec 11 22:51:36 +0000 2012
## 2 NA web FALSE Thu Dec 06 18:31:56 +0000 2012
## 3 NA web FALSE Wed Dec 05 19:51:52 +0000 2012
## 4 NA web FALSE Mon Dec 03 21:53:08 +0000 2012
## 5 NA web FALSE Mon Dec 03 20:25:22 +0000 2012
## 6 NA web FALSE Thu Nov 29 20:05:38 +0000 2012
## 7 NA web FALSE Mon Nov 26 18:41:08 +0000 2012
## 8 NA web FALSE Fri Nov 16 00:07:48 +0000 2012
## 9 NA web FALSE Fri Nov 09 23:20:43 +0000 2012
## 10 NA web FALSE Thu Nov 08 19:44:07 +0000 2012
## in_reply_to_status_id_str in_reply_to_user_id_str lang listed_count
## 1 NA NA NA 11189
## 2 NA NA NA 11189
## 3 NA NA NA 11189
## 4 NA NA NA 11189
## 5 NA NA NA 11189
## 6 NA NA NA 11189
## 7 NA NA NA 11189
## 8 NA NA NA 11189
## 9 NA NA NA 11189
## 10 NA NA NA 11189
## verified location user_id_str
## 1 TRUE San Francisco, CA 6253282
## 2 TRUE San Francisco, CA 6253282
## 3 TRUE San Francisco, CA 6253282
## 4 TRUE San Francisco, CA 6253282
## 5 TRUE San Francisco, CA 6253282
## 6 TRUE San Francisco, CA 6253282
## 7 TRUE San Francisco, CA 6253282
## 8 TRUE San Francisco, CA 6253282
## 9 TRUE San Francisco, CA 6253282
## 10 TRUE San Francisco, CA 6253282
## description
## 1 The Real Twitter API. I tweet about API changes, service issues and happily answer questions about Twitter and our API. Don't get an answer? It's on my website.
## 2 The Real Twitter API. I tweet about API changes, service issues and happily answer questions about Twitter and our API. Don't get an answer? It's on my website.
## 3 The Real Twitter API. I tweet about API changes, service issues and happily answer questions about Twitter and our API. Don't get an answer? It's on my website.
## 4 The Real Twitter API. I tweet about API changes, service issues and happily answer questions about Twitter and our API. Don't get an answer? It's on my website.
## 5 The Real Twitter API. I tweet about API changes, service issues and happily answer questions about Twitter and our API. Don't get an answer? It's on my website.
## 6 The Real Twitter API. I tweet about API changes, service issues and happily answer questions about Twitter and our API. Don't get an answer? It's on my website.
## 7 The Real Twitter API. I tweet about API changes, service issues and happily answer questions about Twitter and our API. Don't get an answer? It's on my website.
## 8 The Real Twitter API. I tweet about API changes, service issues and happily answer questions about Twitter and our API. Don't get an answer? It's on my website.
## 9 The Real Twitter API. I tweet about API changes, service issues and happily answer questions about Twitter and our API. Don't get an answer? It's on my website.
## 10 The Real Twitter API. I tweet about API changes, service issues and happily answer questions about Twitter and our API. Don't get an answer? It's on my website.
## geo_enabled user_created_at statuses_count
## 1 TRUE Wed May 23 06:01:13 +0000 2007 3363
## 2 TRUE Wed May 23 06:01:13 +0000 2007 3363
## 3 TRUE Wed May 23 06:01:13 +0000 2007 3363
## 4 TRUE Wed May 23 06:01:13 +0000 2007 3363
## 5 TRUE Wed May 23 06:01:13 +0000 2007 3363
## 6 TRUE Wed May 23 06:01:13 +0000 2007 3363
## 7 TRUE Wed May 23 06:01:13 +0000 2007 3363
## 8 TRUE Wed May 23 06:01:13 +0000 2007 3363
## 9 TRUE Wed May 23 06:01:13 +0000 2007 3363
## 10 TRUE Wed May 23 06:01:13 +0000 2007 3363
## followers_count favourites_count protected user_url
## 1 1432852 25 FALSE http://dev.twitter.com
## 2 1432852 25 FALSE http://dev.twitter.com
## 3 1432852 25 FALSE http://dev.twitter.com
## 4 1432852 25 FALSE http://dev.twitter.com
## 5 1432852 25 FALSE http://dev.twitter.com
## 6 1432852 25 FALSE http://dev.twitter.com
## 7 1432852 25 FALSE http://dev.twitter.com
## 8 1432852 25 FALSE http://dev.twitter.com
## 9 1432852 25 FALSE http://dev.twitter.com
## 10 1432852 25 FALSE http://dev.twitter.com
## name time_zone user_lang utc_offset
## 1 Twitter API Pacific Time (US & Canada) en -28800
## 2 Twitter API Pacific Time (US & Canada) en -28800
## 3 Twitter API Pacific Time (US & Canada) en -28800
## 4 Twitter API Pacific Time (US & Canada) en -28800
## 5 Twitter API Pacific Time (US & Canada) en -28800
## 6 Twitter API Pacific Time (US & Canada) en -28800
## 7 Twitter API Pacific Time (US & Canada) en -28800
## 8 Twitter API Pacific Time (US & Canada) en -28800
## 9 Twitter API Pacific Time (US & Canada) en -28800
## 10 Twitter API Pacific Time (US & Canada) en -28800
## friends_count screen_name country_code country place_type full_name
## 1 33 twitterapi NA NA NA NA
## 2 33 twitterapi NA NA NA NA
## 3 33 twitterapi NA NA NA NA
## 4 33 twitterapi NA NA NA NA
## 5 33 twitterapi NA NA NA NA
## 6 33 twitterapi NA NA NA NA
## 7 33 twitterapi NA NA NA NA
## 8 33 twitterapi NA NA NA NA
## 9 33 twitterapi NA NA NA NA
## 10 33 twitterapi NA NA NA NA
## place_name place_id place_lat place_lon lat lon expanded_url url
## 1 NA NA NaN NaN NA NA NA NA
## 2 NA NA NaN NaN NA NA NA NA
## 3 NA NA NaN NaN NA NA NA NA
## 4 NA NA NaN NaN NA NA NA NA
## 5 NA NA NaN NaN NA NA NA NA
## 6 NA NA NaN NaN NA NA NA NA
## 7 NA NA NaN NaN NA NA NA NA
## 8 NA NA NaN NaN NA NA NA NA
## 9 NA NA NaN NaN NA NA NA NA
## 10 NA NA NaN NaN NA NA NA NA
# parseTweets 함수는 JSON 형식의 트윗 코퍼스를 연산 및 분석이 가능한 형태의 데이터 프레임으로 변환
# COVID19 hashtag tweets 1004640
# 영어 텍스트
# 중복 트윗 제거
# 5000 random sample
getwd()
## [1] "/Users/shinlee/Dropbox/2020_Class/spring/IntroTM/Rscripts"
load("covid19_tw_sample.RData")
class(covid19_tw_sample)
## [1] "tbl_df" "tbl" "data.frame"
covid19_tw_sample$text[100]
## [1] "Favorite @Simpsons meme I’ve found so far related to work and #COVIDー19 https://t.co/Di8otjSMXJ"
library(stringr)
str_split(str_c(unlist(str_split("가나다 라마바 사아자 차카파타하", pattern=" ")), collapse=" "), pattern=" ")
## [[1]]
## [1] "가나다" "라마바" "사아자" "차카파타하"
str_detect("aafdsa;ak;dj/ifa;dalkj", "A")
## [1] FALSE
str_view_all(covid19_tw_sample$text[10], "[^[:ascii:]]+")
함수 | 설명 | 유사한 기본 함수 |
---|---|---|
str_length() |
문자의 수 | nchar() |
str_split() |
문자열 분리 | strsplit() |
str_c() |
문자열 연결 | paste() |
str_detect() |
문자열의 패턴 유무 인식하여 결과(T/F)를 반환합니다) | none |
str_view_all() |
매칭된 문자열 모두를 보여줌 | none |
stringr
패키지에 있는 모든 함수는 "str_"
로 시작하고 뒤에는 수행 작업과 관련된 용어가 따라옵니다.
대부분의 stringr 함수는 처리하고자 하는 특정 패턴의 텍스트를 찾는데 필요한 정규 표현식과 함께 쓰입니다.
텍스트 사전처리에 유용한 stringr의 함수들을 좀 더 소개해드리자면:
함수 | 정의 |
---|---|
str_trim() |
문자열의 선/후 공백을 제거합니다. |
str_which() |
문자열 벡터에서 일치하는 텍스트 패턴의 모든 위치를 반환합니다. |
str_extract() |
각 문자열 요소에서 처음으로 일치하는 텍스트 패턴을 추출합니다. |
str_extract_all() |
각 문자열 요소에서 일치하는 모든 텍스트 패턴을 추출합니다. |
str_replace() |
각 문자열 요소에서 처음으로 일치하는 텍스트 패턴을 원하는 문자열로 바꿉니다. |
str_replace_all() |
각 문자열에서 일치한는 모든 텍스트 패턴을 원하는 문자열로 바꿉니다. |
library(stringr)
covid19_tw_sample
## # A tibble: 5,000 x 90
## user_id status_id created_at screen_name text source
## <chr> <chr> <dttm> <chr> <chr> <chr>
## 1 435422… 12435823… 2020-03-27 16:55:21 tjhend1 "Hop… Twitt…
## 2 171475… 12436306… 2020-03-27 20:07:17 RedPlaceme… "All… Tweet…
## 3 503711… 12435897… 2020-03-27 17:24:31 ashic "\"N… Twitt…
## 4 594244… 12433836… 2020-03-27 03:45:35 EdAsante77 "Wow… Twitt…
## 5 158782… 12436081… 2020-03-27 18:37:39 JeremyShorr "Bes… Twitt…
## 6 210937… 12435712… 2020-03-27 16:11:06 MensHealth… "Doe… Socia…
## 7 198190… 12433155… 2020-03-26 23:15:00 WALBNews10 "Sou… Socia…
## 8 612066… 12433170… 2020-03-26 23:20:59 npquarterly "Alt… HubSp…
## 9 148841… 12432426… 2020-03-26 18:25:17 bradpatrick "RIP… Twitt…
## 10 119505… 12436207… 2020-03-27 19:27:39 TheCommuni… "3. … Twitt…
## # … with 4,990 more rows, and 84 more variables: display_text_width <dbl>,
## # reply_to_status_id <chr>, reply_to_user_id <chr>,
## # reply_to_screen_name <chr>, is_quote <lgl>, is_retweet <lgl>,
## # favorite_count <int>, retweet_count <int>, quote_count <int>,
## # reply_count <int>, hashtags <list>, symbols <list>, urls_url <list>,
## # urls_t.co <list>, urls_expanded_url <list>, media_url <list>,
## # media_t.co <list>, media_expanded_url <list>, media_type <list>,
## # ext_media_url <list>, ext_media_t.co <list>,
## # ext_media_expanded_url <list>, ext_media_type <chr>,
## # mentions_user_id <list>, mentions_screen_name <list>, lang <chr>,
## # quoted_status_id <chr>, quoted_text <chr>, quoted_created_at <dttm>,
## # quoted_source <chr>, quoted_favorite_count <int>,
## # quoted_retweet_count <int>, quoted_user_id <chr>,
## # quoted_screen_name <chr>, quoted_name <chr>,
## # quoted_followers_count <int>, quoted_friends_count <int>,
## # quoted_statuses_count <int>, quoted_location <chr>,
## # quoted_description <chr>, quoted_verified <lgl>,
## # retweet_status_id <chr>, retweet_text <chr>,
## # retweet_created_at <dttm>, retweet_source <chr>,
## # retweet_favorite_count <int>, retweet_retweet_count <int>,
## # retweet_user_id <chr>, retweet_screen_name <chr>, retweet_name <chr>,
## # retweet_followers_count <int>, retweet_friends_count <int>,
## # retweet_statuses_count <int>, retweet_location <chr>,
## # retweet_description <chr>, retweet_verified <lgl>, place_url <chr>,
## # place_name <chr>, place_full_name <chr>, place_type <chr>,
## # country <chr>, country_code <chr>, geo_coords <list>,
## # coords_coords <list>, bbox_coords <list>, status_url <chr>,
## # name <chr>, location <chr>, description <chr>, url <chr>,
## # protected <lgl>, followers_count <int>, friends_count <int>,
## # listed_count <int>, statuses_count <int>, favourites_count <int>,
## # account_created_at <dttm>, verified <lgl>, profile_url <chr>,
## # profile_expanded_url <chr>, account_lang <lgl>,
## # profile_banner_url <chr>, profile_background_url <chr>,
## # profile_image_url <chr>
covid19_tw_sample$text[1]
## [1] "Hopefully when the White House reopens we’ll have new owners in there too !!!! https://t.co/1EkTXt8nkW"
# 1) standardization
tolower(covid19_tw_sample$text[1])
## [1] "hopefully when the white house reopens we’ll have new owners in there too !!!! https://t.co/1ektxt8nkw"
# 2) space
covid19_tw_sample$text[10]
## [1] "3. @unusual4tune could you tell us a little about the symptoms of COVID-19?\n\nPatients with COVID-19 may have mild to severe respiratory \nillness with symptoms of\n \U0001f912 Fever \n\n \U0001f62b Cough\n \n\U0001f975 Shortness of breath"
str_replace_all(covid19_tw_sample$text[10], "\\s+", " ")
## [1] "3. @unusual4tune could you tell us a little about the symptoms of COVID-19? Patients with COVID-19 may have mild to severe respiratory illness with symptoms of \U0001f912 Fever \U0001f62b Cough \U0001f975 Shortness of breath"
# 3) punctuation
covid19_tw_sample$text[1]
## [1] "Hopefully when the White House reopens we’ll have new owners in there too !!!! https://t.co/1EkTXt8nkW"
str_remove_all(covid19_tw_sample$text[1], "[[:punct:]]+")
## [1] "Hopefully when the White House reopens well have new owners in there too httpstco1EkTXt8nkW"
# 4) number
covid19_tw_sample$text[19]
## [1] "and on the basis of such determination, the Secretary of HHS then declared on March 24, 2020, that circumstances exist justifying the authorization of emergency use of medical devices, including alternative products used as medical devices, during the COVID-19 pandemic"
str_remove_all(covid19_tw_sample$text[19], "\\d+")
## [1] "and on the basis of such determination, the Secretary of HHS then declared on March , , that circumstances exist justifying the authorization of emergency use of medical devices, including alternative products used as medical devices, during the COVID- pandemic"
covid19_tw_sample$text[98]
## [1] "#Myositis patients, join us TONIGHT, Thurs., Mar. 26th, at 7 PM for COVID-19 and Myositis, a Patient Video Support session. Let's discuss what we are feeling and support each other. Add to your calendar. See details https://t.co/Wt1pcWorS7 #inflammatorymyopathy #myositissupport https://t.co/DcfecA3aFl"
str_remove_all(covid19_tw_sample$text[98], "\\d+(st|nd|rd|th)?") # ordinal number
## [1] "#Myositis patients, join us TONIGHT, Thurs., Mar. , at PM for COVID- and Myositis, a Patient Video Support session. Let's discuss what we are feeling and support each other. Add to your calendar. See details https://t.co/WtpcWorS #inflammatorymyopathy #myositissupport https://t.co/DcfecAaFl"
# 5) Non-ASCII code
covid19_tw_sample$text[10]
## [1] "3. @unusual4tune could you tell us a little about the symptoms of COVID-19?\n\nPatients with COVID-19 may have mild to severe respiratory \nillness with symptoms of\n \U0001f912 Fever \n\n \U0001f62b Cough\n \n\U0001f975 Shortness of breath"
str_extract_all(covid19_tw_sample$text[10], "[^[:ascii:]]+")
## [[1]]
## [1] "\U0001f912" "\U0001f62b" "\U0001f975"
library(tidytext)
library(tibble)
library(dplyr)
##
## Attaching package: 'dplyr'
## The following objects are masked from 'package:stats':
##
## filter, lag
## The following objects are masked from 'package:base':
##
## intersect, setdiff, setequal, union
# 1) 온라인 텍스트
covid19_tw_sample$text[11]
## [1] "Our firefighters, along with #firstresponders & #healthcareworkers throughout the Region, continue to serve our communities every day. \nDo your part - #stayhome and be safe.\nHelp prevent the spread of #COVID19.\n\U0001f692 If you do go outside - keep a fire truck width apart! (2 m/6 ft) https://t.co/AjrMprEZYA"
str_split(covid19_tw_sample$text[11], "\\s+")
## [[1]]
## [1] "Our" "firefighters,"
## [3] "along" "with"
## [5] "#firstresponders" "&"
## [7] "#healthcareworkers" "throughout"
## [9] "the" "Region,"
## [11] "continue" "to"
## [13] "serve" "our"
## [15] "communities" "every"
## [17] "day." "Do"
## [19] "your" "part"
## [21] "-" "#stayhome"
## [23] "and" "be"
## [25] "safe." "Help"
## [27] "prevent" "the"
## [29] "spread" "of"
## [31] "#COVID19." "\U0001f692"
## [33] "If" "you"
## [35] "do" "go"
## [37] "outside" "-"
## [39] "keep" "a"
## [41] "fire" "truck"
## [43] "width" "apart!"
## [45] "(2" "m/6"
## [47] "ft)" "https://t.co/AjrMprEZYA"
html_reg <- " |&|<|>|"|#|'"
str_remove_all(covid19_tw_sample$text[11], html_reg)
## [1] "Our firefighters, along with #firstresponders #healthcareworkers throughout the Region, continue to serve our communities every day. \nDo your part - #stayhome and be safe.\nHelp prevent the spread of #COVID19.\n\U0001f692 If you do go outside - keep a fire truck width apart! (2 m/6 ft) https://t.co/AjrMprEZYA"
covid19_tw_sample %>%
slice(11) %>%
mutate(text = str_remove_all(text, html_reg)) %>%
unnest_tweets(word, text) %>%
select(word)
## Using `to_lower = TRUE` with `token = 'tweets'` may not preserve URLs.
## # A tibble: 45 x 1
## word
## <chr>
## 1 our
## 2 firefighters
## 3 along
## 4 with
## 5 #firstresponders
## 6 #healthcareworkers
## 7 throughout
## 8 the
## 9 region
## 10 continue
## # … with 35 more rows
# 2) 축약
covid19_tw_sample$text[1]
## [1] "Hopefully when the White House reopens we’ll have new owners in there too !!!! https://t.co/1EkTXt8nkW"
fix.contractions <- function(text) {
# "won't" is a special case as it does not expand to "wo not"
text <- str_replace_all(text, regex(" won[’']?t ", ignore.case = T), " will not ")
text <- str_replace_all(text, regex(" can[’']?t ", ignore.case = T), " can not ")
text <- str_replace_all(text, regex(" ain[’']?t ", ignore.case = T), " am not ")
text <- str_replace_all(text, regex(" don[’']?t ", ignore.case = T), " do not ")
text <- str_replace_all(text, regex(" doesn[’']?t ", ignore.case = T), " does not ")
text <- str_replace_all(text, regex("n[’']t ", ignore.case = T), " not ")
text <- str_replace_all(text, regex("[’']ll ", ignore.case = T), " will ")
text <- str_replace_all(text, regex("[’']re ", ignore.case = T), " are ")
text <- str_replace_all(text, regex("[’']ve ", ignore.case = T), " have ")
text <- str_replace_all(text, regex("[’']m ", ignore.case = T), " am ")
text <- str_replace_all(text, regex("[’']d ", ignore.case = T), " would ")
text <- str_replace_all(text, regex("[’']s ", ignore.case = T), " ")
return(text)
}
covid19_tw_sample$text[1]
## [1] "Hopefully when the White House reopens we’ll have new owners in there too !!!! https://t.co/1EkTXt8nkW"
sapply(covid19_tw_sample$text[1], fix.contractions)
## Hopefully when the White House reopens we’ll have new owners in there too !!!! https://t.co/1EkTXt8nkW
## "Hopefully when the White House reopens we will have new owners in there too !!!! https://t.co/1EkTXt8nkW"
# 3) URL
url_reg <- "https?[^[:space:]]+|(www.)[^[:space:]]+"
url_ex <- c("https://t.co/1ektxt8nkw", "www.google.com")
str_extract_all(url_ex, regex(url_reg, ignore_case = T))
## [[1]]
## [1] "https://t.co/1ektxt8nkw"
##
## [[2]]
## [1] "www.google.com"
covid19_tw_sample %>%
slice(1) %>%
mutate(text = str_remove_all(text, regex(url_reg, ignore_case = T))) %>%
pull(text)
## [1] "Hopefully when the White House reopens we’ll have new owners in there too !!!! "
# 4) Stop words
covid19_tw_sample %>%
unnest_tweets(word, text) %>%
anti_join(stop_words, by="word") %>%
count(word, sort=T)
## Using `to_lower = TRUE` with `token = 'tweets'` may not preserve URLs.
## # A tibble: 23,016 x 2
## word n
## <chr> <int>
## 1 covid19 1928
## 2 #covid19 1602
## 3 #coronavirus 1025
## 4 people 447
## 5 amp 432
## 6 #covid2019 342
## 7 coronavirus 288
## 8 pandemic 278
## 9 time 233
## 10 health 213
## # … with 23,006 more rows
covid19_tw_sample %>%
mutate(text_new = sapply(text, fix.contractions)) %>%
dplyr::select(text, text_new)
## # A tibble: 5,000 x 2
## text text_new
## <chr> <chr>
## 1 "Hopefully when the White House re… "Hopefully when the White House reo…
## 2 "All governments in the world are … "All governments in the world are t…
## 3 "\"No health service in the world … "\"No health service in the world w…
## 4 "Wow!\nCovid-19 Deaths per million… "Wow!\nCovid-19 Deaths per million:…
## 5 "Best quick summary I’ve read of t… "Best quick summary I have read of …
## 6 "Does fitness play a part in the r… "Does fitness play a part in the re…
## 7 "South Georgia Medical Center, lik… "South Georgia Medical Center, like…
## 8 "Although the world should be focu… "Although the world should be focus…
## 9 "RIP COVID-19 Hero. #covidfallen h… "RIP COVID-19 Hero. #covidfallen ht…
## 10 "3. @unusual4tune could you tell u… "3. @unusual4tune could you tell us…
## # … with 4,990 more rows
html_reg <- " |&|<|>|"|#|'"
url_reg <- "https?[^[:space:]]+|(www.)[^[:space:]]+"
covid19_tw_sample_words <- covid19_tw_sample %>%
mutate(text = sapply(text, fix.contractions)) %>%
mutate(text = sapply(text, function(x) {
str_remove_all(x, "[^[:ascii:]]+")
return(x)})) %>%
mutate(text = sapply(text, function(x) {
str_remove_all(x, html_reg)
return(x)})) %>%
mutate(text = sapply(text, function(x) {
str_remove_all(text, url_reg)
return(x)})) %>%
unnest_tweets(word, text)
## Using `to_lower = TRUE` with `token = 'tweets'` may not preserve URLs.
#anti_join(stop_words, by="word")
covid19_tw_sample_words %>% dplyr::select(user_id, word)
## # A tibble: 128,806 x 2
## user_id word
## <chr> <chr>
## 1 435422713 hopefully
## 2 435422713 when
## 3 435422713 the
## 4 435422713 white
## 5 435422713 house
## 6 435422713 reopens
## 7 435422713 we
## 8 435422713 will
## 9 435422713 have
## 10 435422713 new
## # … with 128,796 more rows
covid19_tw_sample_words %>% count(word, sort=T)
## # A tibble: 23,466 x 2
## word n
## <chr> <int>
## 1 the 4601
## 2 to 3616
## 3 of 2278
## 4 and 2179
## 5 covid19 1931
## 6 in 1871
## 7 a 1834
## 8 #covid19 1603
## 9 is 1593
## 10 for 1507
## # … with 23,456 more rows
지금부터 tibble이라는 데이터 프레임의 형식으로 이루어진 트윗 데이터를 어휘 빈도수 분석을 위한 처리 방식에 대해서 알아보도록 하겠습니다. 이를 위해 필요한 함수를 제공하는 dplyr 그리고 tidytext라는 패키지에 대해서도 살펴보도록 하겠습니다.
자, 그럼 제가 모은 트윗 데이터를 자료실에 올려두었는데요. 각자 다운로드 받도록 하겠습니다. 각자 컴퓨터에 다운로드 받은 파일을 RStudio의 Working Directory 즉, 작업 폴더에 옮기도록 하겠습니다. 이제, load()
함수를 이용해서, 해당 파일을 불러오겠습니다. 자 여기에서 본인이 사용하고 있는 컴퓨터에서 RStudio 세션의 working directory를 확인하는 방법은?
getwd()
## [1] "/Users/shinlee/Dropbox/2020_Class/spring/IntroTM/Rscripts"
load("covid19_tw_sample.RData")
만약, 다른 방법으로 다운로드 받은 파일을 RStudio에 불러오고 싶다면, 위쪽에 위치한 메뉴 탭 중 파일을 선택하고 거기에서 Open File을 선택한 후, 컴퓨터의 Downloads 폴더에 저장이 되어있을 “covid19_tw_sample.RData”파일을 찾아 열기 버튼을 누르면, 지정한 R Data file을 현재 R 환경에 Load할 것인지를 묻는 팝업 메뉴가 뜹니다. 확인 버튼을 누르면, 트윗 5000개를 모아 저장한 R Data File이 로드가 되죠. 이 데이터는 covid19_tw_sample이라는 이름의 객체에 할당이 되어 있습니다.
데이터 파일이 잘 로드가 되었는지 그리고 객체의 구조를 확인하기 위해서, dim()
이라는 함수를 이용해서 covid19_tw_sample
객체의 규모 즉, 열과 행의 규모를 확인해보겠습니다.
dim(covid19_tw_sample) # 데이터 프레임; 5000 행; 90 열
## [1] 5000 90
covid19_tw_sample[1,] # covid19_tw_sample 데이터 프레임의 첫번째 행 (관측치) 인덱싱 (불러오기)
## # A tibble: 1 x 90
## user_id status_id created_at screen_name text source
## <chr> <chr> <dttm> <chr> <chr> <chr>
## 1 435422… 12435823… 2020-03-27 16:55:21 tjhend1 Hope… Twitt…
## # … with 84 more variables: display_text_width <dbl>,
## # reply_to_status_id <chr>, reply_to_user_id <chr>,
## # reply_to_screen_name <chr>, is_quote <lgl>, is_retweet <lgl>,
## # favorite_count <int>, retweet_count <int>, quote_count <int>,
## # reply_count <int>, hashtags <list>, symbols <list>, urls_url <list>,
## # urls_t.co <list>, urls_expanded_url <list>, media_url <list>,
## # media_t.co <list>, media_expanded_url <list>, media_type <list>,
## # ext_media_url <list>, ext_media_t.co <list>,
## # ext_media_expanded_url <list>, ext_media_type <chr>,
## # mentions_user_id <list>, mentions_screen_name <list>, lang <chr>,
## # quoted_status_id <chr>, quoted_text <chr>, quoted_created_at <dttm>,
## # quoted_source <chr>, quoted_favorite_count <int>,
## # quoted_retweet_count <int>, quoted_user_id <chr>,
## # quoted_screen_name <chr>, quoted_name <chr>,
## # quoted_followers_count <int>, quoted_friends_count <int>,
## # quoted_statuses_count <int>, quoted_location <chr>,
## # quoted_description <chr>, quoted_verified <lgl>,
## # retweet_status_id <chr>, retweet_text <chr>,
## # retweet_created_at <dttm>, retweet_source <chr>,
## # retweet_favorite_count <int>, retweet_retweet_count <int>,
## # retweet_user_id <chr>, retweet_screen_name <chr>, retweet_name <chr>,
## # retweet_followers_count <int>, retweet_friends_count <int>,
## # retweet_statuses_count <int>, retweet_location <chr>,
## # retweet_description <chr>, retweet_verified <lgl>, place_url <chr>,
## # place_name <chr>, place_full_name <chr>, place_type <chr>,
## # country <chr>, country_code <chr>, geo_coords <list>,
## # coords_coords <list>, bbox_coords <list>, status_url <chr>,
## # name <chr>, location <chr>, description <chr>, url <chr>,
## # protected <lgl>, followers_count <int>, friends_count <int>,
## # listed_count <int>, statuses_count <int>, favourites_count <int>,
## # account_created_at <dttm>, verified <lgl>, profile_url <chr>,
## # profile_expanded_url <chr>, account_lang <lgl>,
## # profile_banner_url <chr>, profile_background_url <chr>,
## # profile_image_url <chr>
covid19_tw_sample[,1] # covid19_tw_sample 데이터 프레임의 첫번째 열 (변수) 인덱싱 (불러오기)
## # A tibble: 5,000 x 1
## user_id
## <chr>
## 1 435422713
## 2 171475447
## 3 50371199
## 4 594244296
## 5 15878218
## 6 21093744
## 7 19819089
## 8 61206610
## 9 14884156
## 10 1195051598914998273
## # … with 4,990 more rows
“covid19_tw_sample.RData” 로드가 되어있고, 그 데이터 파일이 covid19_tw_sample이라는 객체에 할당되어있음을 확인할 수 있습니다.
하지만, 현재의 데이터 파일은 90개라는 너무 많은 변수들이 포함되어 있어 우리가 원하는, 즉 우리가 처리하고자 하는 트윗의 변수들에 집중하기가 어려운 점이 있습니다. 다시 말해서, 어휘 빈도수 분석에 있어서는 불필요한 변수들은 제거해주는 것이 필요하겠네요. 그리고, 트윗 내용이 중복되는 경우는 삭제해주는 과정 또한 필요하겠네요. 이러한 과정을 수행하는 데이터 처리 작업을 함께 살펴보겠습니다.
자 그러면, 이제부턴 dplyr패키지의 함수들을 이용해서 트위터 데이터를 처리하는 방법을 알아보도록 하겠습니다. 우선, dplyr 패키지를 R세션에 설치해 봅시다. install.packages("dplyr")
library(dplyr)
첫번째로 살펴 볼 dplyr 패키지의 함수는 select()
입니다. 이 함수는 데이터의 변수를 선택해서 해당 변수만 남기고 나머지 변수들은 제거하는 기능을 합니다. 예를 들어서, 우리의 트윗 데이터 객체인 covid19_tw_sample
는 90개의 변수로 구성이 되어있죠. 앞서 말했듯, 이 변수들은 우리가 모은 트윗들의 성격을 규정하는 역할을 합니다. 예를 들어서, 첫번째 변수의 이름을 user_id
이고 이 변수에는 각 트윗의 사용자 아이디가 문자 벡터 형태로 포함되어 있습니다.
# 데이터의 변수 선택은: 데이터 객체 이름에 Dollar 사인 후 변수 이름 입력
class(covid19_tw_sample$user_id) # 문자 벡터
## [1] "character"
covid19_tw_sample$user_id[1:10] # 첫번째 트윗부터 열번째 트윗의 사용자 아이디 인덱싱
## [1] "435422713" "171475447" "50371199"
## [4] "594244296" "15878218" "21093744"
## [7] "19819089" "61206610" "14884156"
## [10] "1195051598914998273"
covid19_tw_sample라는 데이터는 90개의 변수로 구성되어 있고, 여기에서 우리가 필요한 변수는 text
입니다. 이 변수에는 각 트윗의 내용, 즉 우리가 분석할 어휘들로 구성된 트윗 메세지가 문자열 벡터 형태로 포함되어 있습니다. 또, 각 트윗을 구별할 때 필요한 Twitter status 아이디 statu_id
그리고 포스팅 시간을 알려주는 created_at
등이 우리가 필요한 트윗 정보들이라 할 수 있겠네요.
이 변수들만 남기고 나머지 변수들을 제거하기 위해서 필요한 함수가 바로 select()
입니다.
select()
: 데이터의 변수(들)을 선택 및 처리class(covid19_tw_sample$text) #문자(열) 벡터
## [1] "character"
covid19_tw_sample$text[1:10] # 첫번째 트윗부터 열번째 트윗의 문자열 내용 인덱싱
## [1] "Hopefully when the White House reopens we’ll have new owners in there too !!!! https://t.co/1EkTXt8nkW"
## [2] "All governments in the world are trying to manipulate the COVID-19 statistics. I trust countries that allow for free speech and free media. That doesnt mean trust media always being accurate. Question everything, but be open minded and come to your own fact driven conclusions."
## [3] "\"No health service in the world would be able to deal with #coronavirus if the public don't stay indoors...\" Stay in, muppets."
## [4] "Wow!\nCovid-19 Deaths per million:\nGermany: 3\nUSA. 4\n\nWhy is the default in the media, America is bad? https://t.co/MlXSa38wSG"
## [5] "Best quick summary I’ve read of the (temporary) changes to the Ohio educational system due to #COVID19 and HB197. #ohedchat https://t.co/IPjKqwD0UE"
## [6] "Does fitness play a part in the recovery process? Here's what happened https://t.co/UvpOGd26CX"
## [7] "South Georgia Medical Center, like many other Southwest Georgia hospital systems, are releasing their COVID-19 numbers daily. https://t.co/rLV1idTNv9"
## [8] "Although the world should be focusing on how to solve #COVID19, Asian-American advocacy groups and researchers confirm that they have seen a surge of verbal and physical assaults. This needs to stop! https://t.co/bVBxt9kIfd #coronavirus #racism"
## [9] "RIP COVID-19 Hero. #covidfallen https://t.co/8SjgVqlRpA"
## [10] "3. @unusual4tune could you tell us a little about the symptoms of COVID-19?\n\nPatients with COVID-19 may have mild to severe respiratory \nillness with symptoms of\n \U0001f912 Fever \n\n \U0001f62b Cough\n \n\U0001f975 Shortness of breath"
covid19_tw_sample %>% select(status_id, created_at, text) # 데이터 처리 과정을 연쇄적으로 이어주는 파이프(pipe) 연산자; covid19_tw_sample에서 status_id, created_at, text라는 변수들만 남기고 나머지는 제거
## # A tibble: 5,000 x 3
## status_id created_at text
## <chr> <dttm> <chr>
## 1 1243582392390… 2020-03-27 16:55:21 "Hopefully when the White House reop…
## 2 1243630694561… 2020-03-27 20:07:17 "All governments in the world are tr…
## 3 1243589730194… 2020-03-27 17:24:31 "\"No health service in the world wo…
## 4 1243383641310… 2020-03-27 03:45:35 "Wow!\nCovid-19 Deaths per million:\…
## 5 1243608134595… 2020-03-27 18:37:39 "Best quick summary I’ve read of the…
## 6 1243571257884… 2020-03-27 16:11:06 "Does fitness play a part in the rec…
## 7 1243315545962… 2020-03-26 23:15:00 "South Georgia Medical Center, like …
## 8 1243317053026… 2020-03-26 23:20:59 "Although the world should be focusi…
## 9 1243242635134… 2020-03-26 18:25:17 "RIP COVID-19 Hero. #covidfallen htt…
## 10 1243620718958… 2020-03-27 19:27:39 "3. @unusual4tune could you tell us …
## # … with 4,990 more rows
covid19_text <- covid19_tw_sample %>% select(status_id, created_at, text) # 위 처리 과정을 거친 데이터를 covid19_text라는 새로운 객체에 할당
dim(covid19_text) # 새로운 데이터 프레임 구조 확인
## [1] 5000 3
filter()
: 데이터의 관측치(observation)을 선택 및 처리트윗 데이터를 처리하는데 있어, 특정 관측치를 선택할 필요 filter()
함수는 입력된 조건에 부합하는 행들만 남기고 나머지는 지워주는 역할을 합니다. 가령, 어떤 트윗의 텍스트에 특정 단어가 포함되어 있는지 여부에 따라 해당 트윗만 출력하기 위해 필요한 함수는: str_detect()
- 해당 벡터의 값 중 원하는 문자열이 포함되어 있는지 아닌지 판별하는 기능
covid19_text %>% filter(str_detect(text, regex("covid|corona", ignore_case = T)))
## # A tibble: 4,794 x 3
## status_id created_at text
## <chr> <dttm> <chr>
## 1 1243630694561… 2020-03-27 20:07:17 "All governments in the world are tr…
## 2 1243589730194… 2020-03-27 17:24:31 "\"No health service in the world wo…
## 3 1243383641310… 2020-03-27 03:45:35 "Wow!\nCovid-19 Deaths per million:\…
## 4 1243608134595… 2020-03-27 18:37:39 "Best quick summary I’ve read of the…
## 5 1243315545962… 2020-03-26 23:15:00 "South Georgia Medical Center, like …
## 6 1243317053026… 2020-03-26 23:20:59 "Although the world should be focusi…
## 7 1243242635134… 2020-03-26 18:25:17 "RIP COVID-19 Hero. #covidfallen htt…
## 8 1243620718958… 2020-03-27 19:27:39 "3. @unusual4tune could you tell us …
## 9 1243233162412… 2020-03-26 17:47:38 "Our firefighters, along with #first…
## 10 1243317490509… 2020-03-26 23:22:44 "Day 4 of #onlineschool - the new TA…
## # … with 4,784 more rows
4794개의 트윗 텍스트가 “corona” 또는 “covid” 문자열 포함하는 것으로 확인
만약 해당 문자열을 포함하지 않는 트윗을 추출하기 위해 필요한 연산자는: !
! 는 논리 연산에서 부정의 뜻을 의미: ~~이 아니다.
covid19_text %>% filter(!str_detect(text, regex("covid|corona", ignore_case = T)))
## # A tibble: 206 x 3
## status_id created_at text
## <chr> <dttm> <chr>
## 1 1243582392390… 2020-03-27 16:55:21 "Hopefully when the White House reop…
## 2 1243571257884… 2020-03-27 16:11:06 "Does fitness play a part in the rec…
## 3 1243723375320… 2020-03-28 02:15:34 "Thank you. Hated seeing so many peo…
## 4 1243288206344… 2020-03-26 21:26:22 "@Reboticant Both. \nThe email is re…
## 5 1243291113043… 2020-03-26 21:37:55 "hey @HomaTav, look what popped up i…
## 6 1243231200459… 2020-03-26 17:39:50 "@realDonaldTrump I know you don’t r…
## 7 1243584826613… 2020-03-27 17:05:02 "A fantastic resource from the GC He…
## 8 1243648743163… 2020-03-27 21:19:00 "i realize many of you loathe The Wa…
## 9 1243236928058… 2020-03-26 18:02:36 "@CGBadley @ZoeHowerska @BeverleyHug…
## 10 1243313262562… 2020-03-26 23:05:56 "Ontario Libs/NDP must really hate t…
## # … with 196 more rows
text 변수에 문자열 벡터로 담겨있는 텍스트 내용이 “covid” 또는 “corona” 문자열 포함하는 조건에 해당하는 행만 남기고 나머지는 제거
69개의 중복 트윗 텍스트가 확인된 행들은 지워지고 나머지 4,704개의 행만 남음.
covid19_text_unique <- covid19_text %>% filter(str_detect(text, regex("covid|corona", ignore_case = T))) # 위 처리 과정을 거친 데이터를 covid19_text_unique라는 새로운 객체에 할당 *** R은 객체지향 프로그래밍 언어
dim(covid19_text_unique)
## [1] 4794 3
select()
와 filter()
라는 dplyr
패키지의 함수를 이용해서 데이터 처리하는 과정을 살펴봄.
tidytext
자, 이제는 “tidy” 데이터가 무엇인지, 그 원리를 살펴보고 이를 기반으로 한 어휘 빈도수 분석에 효율적인 함수를 제공하는 “tidytext” 패키지를 소개해드리도록 하겠습니다. 특히, 이 패키지의 unnest_tokens()
함수는 텍스트 전처리에 매우 편리한 기능을 제공합니다. 특히, 트윗 텍스트의 경우에는 unnest_tweets()
함수로 편리하게 토큰화 과정이 가능합니다.
자 우선, tidytext
패키지와 텍스트 전처리에 필요한 stringr
패키지를 R세션에 설치해 봅시다.
library(tidytext)
library(stringr)
앞서 우리는 영어 텍스트를 이용한 어휘 빈도수 분석을 위해서, 여러 전처리 과정을 거쳐왔습니다. 예를 들어, 문자열을 소문자로 통일시키고, 텍스트에서 구두점, 숫자, 알파벳 이외의 문자 등을 정규 표현을 이용해서 매칭하고 지워주거나 바꿔주는 전처리 작업을 했었죠. 그리고 이렇게 전처리 과정을 거친 영어 텍스트를 단어 단위로 분석하기 위해서 빈칸을 기준으로 문자열을 쪼개는 작업을 거쳤습니다. 이를 위해 str_split()
이라는 함수를 사용했었죠.
tidytext
패키지의 unnest_tokens()
함수는 이 과정을 한번에 그리고 편리하게 수행할 수 있도록 해주는 기능을 수행합니다. apostrophe의 축약형을 제외한 텍스트의 구두점은 자동적으로 삭제하고, 알파벳을 소문자로 변환하고 빈칸을 기준으로 단어 단위로 텍스트를 쪼개서 그 결과값을 ‘tidy’ 데이터로 만들어 줍니다. 결국, 텍스트를 토큰 단위로 쪼개서 한 행에 하나의 토큰, 여기에선 한 단어씩 위치하게하는 데이터 프레임 형식으로 변환하는 기능을 수행하는 거죠.
tidy
데이터란 각 행에 하나의 토큰(여기에선 하나의 단어)만 위치하도록 구성된 테이블을 말합니다. 결국 tidy
한 데이터를 만든다는 것은 토큰의 단위를 정하고 텍스트 데이터를 토큰화하여 각 행이 하나의 토큰으로 구성되게 만든다는 것이죠. 이렇게 tidy
데이터를 구성한다는 것은 각 행이 하나의 단어씩 가지고 있기 때문에 이후 어휘 빈도수 분석 또는 사전기반 감정분석을 수행할 때 편리하게 연계 작업할 수 있다는 장점이 있습니다.
자, 다음의 R코드는 unnest_tokens()
함수를 이용해서 covid19_text_unique
라는 트윗 데이터를 토큰화한 과정을 보여주는데요.
covid19_text_unique %>% arrange(created_at) # dplyr의 arrange() 함수는 해당 변수를 순차적으로 정렬 (작은 것부터); 시간의 경우 오래된 데이터부터 정렬
## # A tibble: 4,794 x 3
## status_id created_at text
## <chr> <dttm> <chr>
## 1 1243224948870… 2020-03-26 17:15:00 "Trump Cabinet's Bible teacher says …
## 2 1243225028264… 2020-03-26 17:15:19 "How does one #StayAtHome in a soci…
## 3 1243225073684… 2020-03-26 17:15:30 "\"Covidguilt\"...the guilt you feel…
## 4 1243225136515… 2020-03-26 17:15:45 "In order to support residents who a…
## 5 1243225248356… 2020-03-26 17:16:11 "@AirbnbHelp I am aware of that page…
## 6 1243225287594… 2020-03-26 17:16:21 "@RehamKhan1 Covid-19 therapy will b…
## 7 1243225376161… 2020-03-26 17:16:42 "COVID-19 AND NIGERIA POLITICAL ELIT…
## 8 1243225400316… 2020-03-26 17:16:48 "I love this!! \n\nEveryone pull out…
## 9 1243225459061… 2020-03-26 17:17:02 "@RealCandaceO swine flu has death r…
## 10 1243225530901… 2020-03-26 17:17:19 "The Rich staying rich. Our fault fo…
## # … with 4,784 more rows
covid19_text_unique %>% arrange(created_at) %>% unnest_tokens(word, text, token = "words") %>% slice(11:30) # dplyr의 slice() 함수는 특정 범위의 행(들)만 추출해서 보여주는 기능을 함
## # A tibble: 20 x 3
## status_id created_at word
## <chr> <dttm> <chr>
## 1 1243224948870533120 2020-03-26 17:15:00 covid
## 2 1243224948870533120 2020-03-26 17:15:00 19
## 3 1243224948870533120 2020-03-26 17:15:00 blog
## 4 1243224948870533120 2020-03-26 17:15:00 post
## 5 1243224948870533120 2020-03-26 17:15:00 https
## 6 1243224948870533120 2020-03-26 17:15:00 t.co
## 7 1243224948870533120 2020-03-26 17:15:00 ws4c8ztqj1
## 8 1243225028264681472 2020-03-26 17:15:19 how
## 9 1243225028264681472 2020-03-26 17:15:19 does
## 10 1243225028264681472 2020-03-26 17:15:19 one
## 11 1243225028264681472 2020-03-26 17:15:19 stayathome
## 12 1243225028264681472 2020-03-26 17:15:19 in
## 13 1243225028264681472 2020-03-26 17:15:19 a
## 14 1243225028264681472 2020-03-26 17:15:19 society
## 15 1243225028264681472 2020-03-26 17:15:19 that
## 16 1243225028264681472 2020-03-26 17:15:19 depends
## 17 1243225028264681472 2020-03-26 17:15:19 on
## 18 1243225028264681472 2020-03-26 17:15:19 everyone
## 19 1243225028264681472 2020-03-26 17:15:19 being
## 20 1243225028264681472 2020-03-26 17:15:19 out
이 코드는 covid19_text_unique
는 데이터를 unnest_tokens()
함수를 이용하여, text
라는 변수의 문자열을 단어 단위로 쪼개서 word
라는 새로운 변수에 저장해주라는 명령을 수행합니다. 여기에서 토큰화 단위는 즉, 문자열을 쪼개고자 하는 기본 단위는 token
이라는 인자를 “words”로 설정했죠. 이를 통해 단어 단위로 문자열을 토큰화하라는 설정이 취해진 것입니다.
unnest_tokens()
함수의 기능은 문자열을 단어 단위로 토큰화하라는 것 이외에 정규 표현식을 이용해서 텍스트를 토큰화하도록 할 수 있다는 점인데요. 따라서 텍스트를 쪼개는 토큰의 단위를 다양하게 설정할 수 있는 장점이 있습니다. 예를 들어서, 우리가 다루는 트윗 데이터에 경우, 텍스트에 우물 정자로 표시되는 해시태그나 골뱅이 모양으로 표시되는 트위터 핸들이 많이 포함되어 있는데요. 이러한 기호들은 트윗에서 그 자체로서 특별한 의미를 지니고 있기 때문에, 다른 구두점이나 기호들과는 차별해서 처리해 줘야 하는 경우가 있습니다. 따라서 해시태그와 트위터 핸들의 표시는 지우지 않는 토큰화 방법이 필요하고, 이 때 unnest_tokens()
는 유용한 기능을 합니다.
토큰화에서 문자열을 쪼개는 기준을 특별히 정하기 위해서는 token
인자를 words
가 아닌 regex
로 설정하고, pattern
인자를 기준이 될 문자열 패턴을 정해주면 됩니다. 가령, 빈칸을 기준으로 문자열을 쪼개고 싶다는 가정하에 다음의 코드를 실행해 보도록 하겠습니다.
covid19_text_unique %>% arrange(created_at) %>% unnest_tokens(word, text, token = "regex", pattern=" ") %>% slice(11:30) # 해시태그나 트위터 핸들의 의미는 보존하지만, 빈칸이 아닌 공백문자를 기준으로 문자열을 쪼개지는 못함
## # A tibble: 20 x 3
## status_id created_at word
## <chr> <dttm> <chr>
## 1 1243224948870533120 2020-03-26 17:15:00 "covid-19"
## 2 1243224948870533120 2020-03-26 17:15:00 "blog"
## 3 1243224948870533120 2020-03-26 17:15:00 "post\nhttps://t.co/ws4c8ztqj1"
## 4 1243225028264681472 2020-03-26 17:15:19 "how"
## 5 1243225028264681472 2020-03-26 17:15:19 "does"
## 6 1243225028264681472 2020-03-26 17:15:19 "one"
## 7 1243225028264681472 2020-03-26 17:15:19 "#stayathome"
## 8 1243225028264681472 2020-03-26 17:15:19 "in"
## 9 1243225028264681472 2020-03-26 17:15:19 "a"
## 10 1243225028264681472 2020-03-26 17:15:19 "society"
## 11 1243225028264681472 2020-03-26 17:15:19 "that"
## 12 1243225028264681472 2020-03-26 17:15:19 "depends"
## 13 1243225028264681472 2020-03-26 17:15:19 "on"
## 14 1243225028264681472 2020-03-26 17:15:19 "everyone"
## 15 1243225028264681472 2020-03-26 17:15:19 "being"
## 16 1243225028264681472 2020-03-26 17:15:19 "out??\n#covid19kenya"
## 17 1243225028264681472 2020-03-26 17:15:19 "#komeshacorona"
## 18 1243225028264681472 2020-03-26 17:15:19 "#covid19"
## 19 1243225073684807682 2020-03-26 17:15:30 "\"covidguilt\"...the"
## 20 1243225073684807682 2020-03-26 17:15:30 "guilt"
결국, pattern
이라는 인자에는 토큰화에서 제외시켜야 할 모든 문자가 포함되어야 함
covid19_text_unique %>% arrange(created_at) %>% unnest_tokens(word, text, token = "regex", pattern="[^[:word:]#@]") %>% slice(11:30) # 단어를 구성하는 알파벳, 숫자 및 해시태그 트위터핸들 기호를 제외한 모든 문자
## # A tibble: 20 x 3
## status_id created_at word
## <chr> <dttm> <chr>
## 1 1243224948870533120 2020-03-26 17:15:00 wrath
## 2 1243224948870533120 2020-03-26 17:15:00 in
## 3 1243224948870533120 2020-03-26 17:15:00 covid
## 4 1243224948870533120 2020-03-26 17:15:00 19
## 5 1243224948870533120 2020-03-26 17:15:00 blog
## 6 1243224948870533120 2020-03-26 17:15:00 post
## 7 1243224948870533120 2020-03-26 17:15:00 https
## 8 1243224948870533120 2020-03-26 17:15:00 t
## 9 1243224948870533120 2020-03-26 17:15:00 co
## 10 1243224948870533120 2020-03-26 17:15:00 ws4c8ztqj1
## 11 1243225028264681472 2020-03-26 17:15:19 how
## 12 1243225028264681472 2020-03-26 17:15:19 does
## 13 1243225028264681472 2020-03-26 17:15:19 one
## 14 1243225028264681472 2020-03-26 17:15:19 #stayathome
## 15 1243225028264681472 2020-03-26 17:15:19 in
## 16 1243225028264681472 2020-03-26 17:15:19 a
## 17 1243225028264681472 2020-03-26 17:15:19 society
## 18 1243225028264681472 2020-03-26 17:15:19 that
## 19 1243225028264681472 2020-03-26 17:15:19 depends
## 20 1243225028264681472 2020-03-26 17:15:19 on
자, 이러한 tidytext
방식의 토큰화를 거치면, 트윗 텍스트가 어휘 단위로 구분된 새로운 데이터를 얻게 되는데요. 이를 이용하면 어휘 빈도수 분석이 매우 용이합니다.
unnest_regex <- "[^[:word:]#@]"
covid19_word_count <- covid19_text_unique %>%
unnest_tokens(word, text, token = "regex", pattern = unnest_regex) %>%
count(word, sort = TRUE)
dplyr의 count() 함수는 해당 변수 즉 벡터를 구성하는 값들의 빈도수를 계산합니다. sort 인자는 빈도수를 내림차순을 정렬할 수 있게 합니다.
단어 빈도수가 데이터 형식으로 저장된 covid19_word_count의 내용을 인덱싱
covid19_word_count[1:10,] #`covid19_word_count`는 데이터 프레임의 형식을 갖추고 있다. 변수는 두개. 관측값은 16,730. 즉, 트윗에서 나타나는 고유 단어의 수
## # A tibble: 10 x 2
## word n
## <chr> <int>
## 1 the 4471
## 2 t 4193
## 3 co 3627
## 4 https 3615
## 5 to 3495
## 6 of 2226
## 7 19 2215
## 8 covid 2137
## 9 and 2108
## 10 in 1830
covid19_word_count[41:50,] # 데이터 프레임 인덱싱 방법, 대괄호에 쉼표 앞에 숫자 또는 그 범위는 해당 행의 범위, 쉼표 뛰는 해당 열의 범위
## # A tibble: 10 x 2
## word n
## <chr> <int>
## 1 my 402
## 2 cases 382
## 3 they 364
## 4 about 362
## 5 how 358
## 6 if 356
## 7 now 347
## 8 #covid2019 346
## 9 us 329
## 10 more 327
위의 방법으로 우리는 #covid19을 포함한 트윗 텍스트를 단어 단위로 토큰화한 결과의 어휘 빈도수를 확인할 수 있습니다. 여기에서 가장 많이 등장하는 단어들이 대체로 그 이유가 이해가 됩니다만, 몇 단어들은 타당하지 않거나 맥락에서 벗어나 있는 경우들도 있습니다. 가령 “t”,“co”,“https”와 같은 단어들은 covid19에 관련한 단어라고 볼 수 없고, 그 자체로서도 어휘적 의미를 갖고 있지 않죠. 이 단어들은 사실, 트윗에 있는 url을 구성하는 문자열입니다. 트윗 내용의 의미를 결정하는 단어들이 아닌 것이죠. 또한 비 알파벳 문자인 한글 또는 한자 문자열이 제거되지 않은 점도 해결해야 함.
unnest_tokens()
함수로는 처리되지 않는 네 가지 해결해야 할 문제 1. URL은 처리(제거)되지 않았음 2. 트윗 텍스트에 종종 포함되어 있는 HTML 특수문자, 즉 웹페이지에서 표현되는 특기호들을 표시하는 다음의 문자들(& = &
, < = <
, > = >
, " = "
)도 제거되어야 합니다.
또한 3. 알파벳 이외의 한글, 한자등의 문자 또한 처리(제거)되지 않았음 (non-ASCII characters)
마지막으로, 4. 불용어 (stopwords)들도 제거하면 좋겠죠.
위 네가지 해결해야 할 부분들을 어떻게 데이터 처리 과정에 포함시킬지 함께 살펴보겠습니다.
remove_regex <- "https?://[[:word:]./]+|www\\.[[:word:]./]+|&|<|>|""
covid19_text_unique %>% slice(10) %>% mutate(text = str_replace_all(text, remove_regex, "")) # text 변수의 문자열에서 url 문자열 또는 HTML 특수기호 문자열을 매칭하는 정규표현식
## # A tibble: 1 x 3
## status_id created_at text
## <chr> <dttm> <chr>
## 1 1243317490509… 2020-03-26 23:22:44 "Day 4 of #onlineschool - the new TA …
covid19_text_unique %>% slice(10) %>% mutate(text = str_replace_all(text, "[#@]?[^[:ascii:]]", "")) # text 변수의 문자열에서 해시태그, 트위터핸들이 포함된 비알파벳 문자를 제거
## # A tibble: 1 x 3
## status_id created_at text
## <chr> <dttm> <chr>
## 1 1243317490509… 2020-03-26 23:22:44 Day 4 of #onlineschool - the new TA m…
covid19_tidy <- covid19_text_unique %>%
mutate(text = sapply(text, function(x) {str_replace_all(x, remove_regex, "")
return(x)})) %>%
mutate(text = sapply(text, function(x) {str_replace_all(x, "[#@]?[^[:ascii:]]", "")
return(x)})) %>%
unnest_tweets(word, text) %>%
filter(!word %in% stop_words$word) # Matching 연산자 %in% ; tidytext 패키지는 불용어 리스트를 데이터 프레임 형식으로 제공해줌
## Using `to_lower = TRUE` with `token = 'tweets'` may not preserve URLs.
covid19_tidy
## # A tibble: 65,944 x 3
## status_id created_at word
## <chr> <dttm> <chr>
## 1 1243630694561128452 2020-03-27 20:07:17 governments
## 2 1243630694561128452 2020-03-27 20:07:17 world
## 3 1243630694561128452 2020-03-27 20:07:17 manipulate
## 4 1243630694561128452 2020-03-27 20:07:17 covid19
## 5 1243630694561128452 2020-03-27 20:07:17 statistics
## 6 1243630694561128452 2020-03-27 20:07:17 trust
## 7 1243630694561128452 2020-03-27 20:07:17 countries
## 8 1243630694561128452 2020-03-27 20:07:17 free
## 9 1243630694561128452 2020-03-27 20:07:17 speech
## 10 1243630694561128452 2020-03-27 20:07:17 free
## # … with 65,934 more rows
covid19_tidy_count <- covid19_tidy %>% count(word, sort=T)
covid19_tidy_count
## # A tibble: 22,354 x 2
## word n
## <chr> <int>
## 1 covid19 1928
## 2 #covid19 1602
## 3 #coronavirus 1025
## 4 people 428
## 5 amp 418
## 6 #covid2019 342
## 7 coronavirus 288
## 8 pandemic 269
## 9 time 225
## 10 health 206
## # … with 22,344 more rows
library(wordcloud)
## Loading required package: RColorBrewer
set.seed(816)
pal <- brewer.pal(8, "Dark2")
wordcloud(words = covid19_tidy_count$word,
freq = covid19_tidy_count$n,
max.words = 200,
random.order =FALSE,
rot.per = 0.1,
scale = c(4, 0.4),
colors = pal)