11 апреля 2012

Data Mining при помощи R: пример анализа twitter-сообщений



Сервис Twitter сегодня приобретает все большую популярность. В связи со своим небольшим размером, twitter-сообщения, как правило, обладают повышенной содержательностью, что делает их привлекательными с точки зрения контент-анализа и выявления трендов (например, для предсказания вспышек гриппа, предсказания биржевых событий, анализа настроений в социальных группах, скорости распространения информации, и т.д.). Разработчики сервиса предоставляют открытый интерфейс программирования приложений (API), позволяющий достаточно легко извлекать тексты сообщений требуемой тематики из базы данных Twitter'а для их последующего анализа. Конечно, эта возможность не была пропущена пользователями R. В Сети можно найти немало очень интересных образцов применения R для извлечения и анализа twitter-текстов (например, анализ удовлетворенности пассажиров сервисом разных авиакомпаний). Ниже я приведу пример twitter-анализа и попутно расскажу о нескольких используемых для этого R-пакетах.

Представленный ниже R-код был заимстован с нескольких сайтов, в частности RDataMining, OUseful.Info, и One R Tip A Day, но несколько видоизменен мной с учетом особенностей работы с кириллическим текстом. Примером послужат сообщения белорусских пользователей Twitter'а, опубликованные в день написания данной статьи (11 апреля 2012 г.). Позже станет понятнее, почему я выбрал именно этот сегмент пользователей сервиса и именно в этот день.

Для сбора сообщений мы воспользуемся пакетом twitteR (если он на Вашем компьютере не установлен, выполните команду install.packages("twitterR")). (Внимание: приведенный ниже код был полностью рабочим на момент написания этой статьи. Однако начиная с июня 2012 г., способ доступа к API сервиса Twitter из среды R, к сожалению, изменился в сторону усложнения. Как следствие, описанный здесь простой способ сбора сообщений больше работать не будет. См. дополнительную статью о том, как с этим бороться). Одной из "рабочих лошадок" этого пакета является функция searchTwitter(), основным аргументом которой служит searchString - текстовый вектор, определяющий поисковую фразу. Для рассматриваемой задачи я решил выполнять поиск по хэштегу #twiby - одному из основных тегов, используемых пользователями из Беларуси (подробнее можно узнать на сайте twitter.by). Кроме поисковой фразы, функция searchTwitter() позволяет выбрать максимальное количество сообщений, которое будет извлечено в результате поиска из базы данных Twitter'а (аргумент n), а также задать временной диапазон для дат публикации извлекаемых сообщений (аргументы since (с) и until (до)). Здесь важно отметить, что бесплатный API от Twitter имеет ограничение на максимальное количество извлекаемых сообщений - 1500 единиц. Однако на практике этого оказывается вполне достаточно. Обратим внимание также на аргумент lang (от language - язык), который используется для спецификации языка извлекаемых сообщений. Для обозначения языка используется стандарт ISO 639-1, т.е. в случае с русским языком имеем "ru". Итак:

require(twitteR)
 
# Задаем поисковую фразу:
searchTerm="#twiby"
 
# Выполняем поиск и сохраняем результаты в объекте rdmTweets.
# В качестве даты публикации сообщений указываем текущую (команда Sys.Date()).
# На момент выполнения поиска это 11 апреля 2012 г. (~ 7 часов вечера)
 
rdmTweets <- searchTwitter(searchTerm, n = 1500, lang="ru",
                            since = as.character(Sys.Date()))
 
# Выясним размер полученного объекта:
length(rdmTweets)

Созданный нами объект rdmTweets является списком. Каждый элемент этого списка содержит отдельное сообщение, например:

rdmTweets[[1]]
[1] "elvis_oreshek: Только что пришла с пробежки. Ох, детка, мышцы так устали!
Офигенное ощущение.(кто занимается спортом - поймет о чем я) #twiby #Russia"
 
rdmTweets[[2]]
[1] "t_webskins: #БТлжёт #twiby"

Для удобства дальнейшей работы с этими сообщениями список rdmTweets стоит преобразовать в таблицу данных. Эта операция легко выполняется при помощи функции twListToDF() из пакета twitteR:

tw.df <- twListToDF(rdmTweets)

В некоторых из собранных нами сообщений почти наверняка будут встречаться имена пользователей, начинающиеся со знака @. Очевидно, что такого рода информация является "шумовой" и не представляет большого интереса (по-крайней мере, в данном анализе). От имен пользователей можно избавиться, предварительно создав для этого небольшую функцию, которая будет искать регулярные выражения, начинающиеся с @, и удалять их:

# Функция для удаления пользовательских имен:
RemoveAtPeople <- function(tweet) {
    gsub("@\\w+", "", tweet)}
 
# Удаляем пользовательские имена:
tweets <- as.vector(sapply(tw.df$text, RemoveAtPeople))

Далее нам нужен будет пакет tm (от text mining), имеющий широкие возможности для анализа текстовой информации (как обычно, его можно установить при помощи команды install.packages("tm")). Обработка twitter-сообщений, хранящихся теперь в текстовом векторе tweets, потребует нескольких функций из этого пакета. Первым шагом должно стать создание т.н. "корпуса", т.е. сведение всех имеющихся небольших текстов (сообщений) в один объект:

tw.corpus <- Corpus(VectorSource(tweets))
 
tw.corpus
A corpus with 542 text documents

С корпусом текстов можно выполнять разнообразные операции, что осуществляется через интерфейс функции tm_map() из пакета tm. Для начала стоит удалить из анализируемых сообщений все знаки пунктуации, которые не несут никакой смысловой нагрузки (функция removePunctuation()):

tw.corpus <- tm_map(tw.corpus, removePunctuation)

Теперь сделаем так, чтобы все слова в анализируемых сообщениях были представлены прописными буквами:

tw.corpus <- tm_map(tw.corpus, tolower)

Следующий шаг - очень важный. Он состоит в том, чтобы удалить из сообщений как можно больше т.н. "стоп-слов", или "шумовых слов", т.е. слов, не несущих смысловой нагрузки (например, "это", "я", "мы", "тут", "возможно" "один", "два", и т.п.). Если бы мы работали с англоязычными текстами, можно было бы воспользоваться хорошо работающим готовым решением из пакета tm: tw.corpus <- tm_map(tw.corpus, removeWords, stopwords("english")).  Теоретически, в приведенной команде можно было бы заменить "english" на "russian", но практика показывает, что это дает неудовлетворительные результаты. Однако имеется возможность применить и пользовательский список стоп-слов. Один из таких списков я нашел здесь (скачать его в виде текстового файла можно здесь). После загрузки этого списка в R и сохранения его под именем my.stopwords (в виде текстового вектора), выполняем следующую команду:

tw.corpus <- tm_map(tw.corpus, removeWords, my.stopwords)

Все приведенные команды, использующие функцию tm_map(), можно объединить в одну более крупную функцию, которую будет удобно использовать при выполнении аналогичных анализов в будущем. Например:

generateCorpus <- function(df, my.stopwords = c()) {
        tw.corpus <- Corpus(VectorSource(df))
        tw.corpus <- tm_map(tw.corpus, removePunctuation)
        tw.corpus <- tm_map(tw.corpus, tolower)
        tw.corpus <- tm_map(tw.corpus, removeWords, my.stopwords)
 
    tw.corpus
}

Эта новая функция позволяет быстро подготовить корпус twitter-сообщений для дальнейшего анализа, например, так:

my.corpus <- generateCorpus(tweets, my.stopwords)

Далее будем работать именно с объектом my.corpus. В этой статье я покажу, как выполнить контент-анализ twitter-сообщений путем создания и визуального анализа облака наиболее часто встречающихся в этих сообщениях слов. Для создания облака слов воспользуемся возможностями пакета wordcloud (установите при помощи команды install.packages("wordcloud"); потребуется также пакет RColorBrewer для создания палитры цветов - установите и его). Подобно функции generateCorpus(), создадим для удобства работы функцию generateWordcloud():

generateWordcloud <- function(corpus, min.freq = 3, ...) {
    require(wordcloud)
 
# Преобразовываем "корпус" в матрицу индексируемых слов из сообщений.
# Подробнее о структуре и назначении этой матрицы см. здесь    
doc.m <- TermDocumentMatrix(corpus,
        control = list(minWordLength = 1))
dm <- as.matrix(doc.m)
 
# Подсчитываем частоту слов:
    v <- sort(rowSums(dm), decreasing = TRUE)
    d <- data.frame(word = names(v), freq = v)
 
    require(RColorBrewer)
    pal <- brewer.pal(8, "Accent")
 
# Генерируем облако слов:
    wc <- wordcloud(d$word, d$freq, min.freq = min.freq, colors = pal)
    wc
}

Теперь у нас все готово для создания облака слов. Но перед тем как его нарисовать, следует еще раз вернуться к стоп-словам. Дело в том, что помимо упомянутых выше шумовых слов, в анализируемых нами сообщениях следует ожидать и ряда специфичных "шумов". Предварительный анализ показал, что стоит удалить как минимум следующие слова:

stops <- c("belarus", "minsk", "twiby", "8pua",
  "минск", "беларусь", "беларуси")

Эти дополнительные стоп-слова мы можем лекго добавить к стандартным при вызове функции generateCorpus():

my.corpus <- generateCorpus(tweets, с(my.stopwords, stops))

Создаем облако слов (число 15 в приведенной ниже команде соответствует минимальной частоте встречаемости, ниже которой то или иное слово в облако включено не будет; очевидно, что варьируя этот порог, можно получать несколько различающиеся варианты облаков):

print(generateWordcloud(my.corpus), 15)



Как видно из рисунка, одним из ключевых слов в сообщениях, опубликованных белорусскими пользователями сервиса Twitter 11 апреля 2012 г. примерно к 7 часам вечера, было minskblast. Фактически, это слово было хэштегом, которым отмечались многие сообщения в этот день. Вторая часть в этом составном слове - blast - значит "взрыв". Как известно, ровно год назад в минском метро произошло трагическое событие - теракт, в котором погибли 15 человек. Безусловно, это событие не могло не стать важной темой для обсуждений в белорусском обществе 11 апреля в этом году, что отразилось и в сообщениях среди белорусских пользователей Twitter'a.



Светлая память погибшим.




25 комментариев :

Itman комментирует...

Это хорошо, но массово этим не попользуешься, у эйпиая есть ограничения на число запросов в секунду.

Unknown комментирует...

Есть статья на Хабре на аналогичную тему http://habrahabr.ru/post/140093/

Mariia Oblasova комментирует...

Прекрасный пример, спасибо большое! Я попыталась применить его для своих целей, но у меня возникли проблемы, может быть Вы смогли бы мне помочь! Я не убирала стоп и шумовые слова, но у меня из слов попрападали украинские буквы і и ч. С чем это связано?

Mariia Oblasova комментирует...

Я нашла причину - это TermDocumentMatrix ! Чем мне можно заменить эту функцию?

iDen комментирует...

Правильно я понимаю, что требуется предварительная авторизация? У меня такое сообщение: Ошибка в twInterfaceObj$doAPICall(cmd, params, "GET", ...) :
OAuth authentication is required with Twitter's API v1.1

Sergey Mastitsky комментирует...

Да, начиная с марта 2013 г. Twitter требует авторизации всех приложений, использующих их API: https://dev.twitter.com/blog/changes-coming-to-twitter-api
О том, как обойти это ограничение программно, написано в руководстве пользователя пакета twitteR: http://cran.r-project.org/web/packages/twitteR/vignettes/twitteR.pdf

Sergey Mastitsky комментирует...

Мария, похоже, Вы просто не загрузили пакет tm, в состав которого входит эта функция.

iDen комментирует...

Что-то у меня такая ситуация:

> cred <- OAuthFactory$new(consumerKey="blabla", consumerSecret="blablabla", requestURL="https://api.twitter.com/oauth/request_token", accessURL="https://api.twitter.com/oauth/access_token", authURL="https://api.twitter.com/oauth/authorize" )
> cred$handshake()
Ошибка в function (type, msg, asError = TRUE) :
SSL certificate problem, verify that the CA cert is OK. Details:
error:14090086:SSL routines:SSL3_GET_SERVER_CERTIFICATE:certificate verify failed

Mariia Oblasova комментирует...

я его как раз загрузила, но мне сама функция TermDocumentMatrix не нужна - так как она оставляет только основу слова, убирая всемозможные -ия и тд !

iDen комментирует...

Заменил https на http как тут https://dev.twitter.com/discussions/1596, но всё равно ошибка авторизации - Ошибка: Unauthorized

Анонимный комментирует...

Огромное спасибо! Вопрос стыдный (хелпов в Интернете полно), но плохо разбираюсь в работе с памятью: получил вектор из отдельных слов (более 50 тыс.). При попытке построить облако - ругается на нехватку памяти. Так и не смог разобраться - как применять команду для расширения доступной памяти.
PS Debian Linux 64x, граф. интерфейсом не пользуюсь - работаю через эмулятор консоли ("терминал GNOME")

Sergey Mastitsky комментирует...

Посмотрите здесь: http://answers.stat.ucla.edu/groups/answers/wiki/0e7c9/Increasing_Memory_in_R.html
Обратите также внимание на функцию gc() ("сборщик мусора")

Анонимный комментирует...

Сергей, спасибо за наглядный пример.
Хотелось бы узнать, есть ли в свободном доступе информация для более подробного изучения Text Mining: книги, блоги, видеокурсы и т. п. Что можете посоветовать?

И ещё давно интересует вопрос о том, как и какими средствами происходит сбор данных. В данном примере Вы указали Twitter, который предоставляет доступ к сообщениям, но ведь как-то анализируют информацию из социальных сетей, новостных ресурсов, блогов и так далее. То есть, анализ анализом, но как эта информация собирается и импортируется в R?

Sergey Mastitsky комментирует...

По поводу Вашего первого вопроса: свободно доступной информации в Сети по этой теме очень много - Гугл в помощь, как говорится. Если конкретно интересует информация по R - начните с поиска на r-bloggers.com и stackoverflow.com. Кроме того, на CRAN есть целый раздел, где перечислены основыне пакеты для анализа текстовых данных: http://cran.r-project.org/web/views/NaturalLanguageProcessing.html

Вторй вопрос: есть несколько пакетов, обеспечивающих "связь" R с социальными сервисами, в частности рассмотренный в этом пример twitteR и Rfacebook. Насчет новостных ресурсов - не знаю.

Анонимный комментирует...

Сергей, я хотел бы поделится любопытными наблюдениями, с которыми столкнулся при работе с русскоязычным текстом.

1. При очистке текста от знаков пунктуации, цифр, и т. д. имеет место проблема "слипания соседних слов".
Например: при чистке строки, которая содержит "и/или", функция removePunctuation() преобразует эту часть строки в "иили". Таже проблема возникает, если в сообщении пропущен пробел после запятой, точки и любого другого знака пунктуации. Тоже самое касается чисел, которые встроены в текст без пробела.
Решил эту проблему с помощью функции str_replace_all() из пакета "stringr". В качестве замены символов выбираю пробел, а затем с помощью функции str_trim() удаляю лишние пробелы. Так слова остаются разделены, но при этом не слипаются. Правда делать все это приходится со строками не формируя корпус.

2. Буквы "ё" и "ч", почему-то, классифицируются R как знаки пунктуации - они удаляются из текстовых строк, при чистке пунктуаций. Над решением этой проблемы пока не думал.

Интересно узнать Ваше мнение по этим комментариям. Может я слишком все усложняю, и на самомо деле есть простое решение этих неудобств.

Sergey Mastitsky комментирует...

Спасибо за полезную информацию!
Опыта работы с русскоязычными текстами у меня не много, но мне уже попадались описания проблем с "ё", "ч" и еще "я", подобные Вашим. К сожалению сейчас уже не вспомню, где я это видел. Эти проблемы могут быть вызваны разными причинами (кодировка, операционная система, особенности той или иной функции и т.п.) и поэтому сложно дать какое-то готовое и простое решение.
В принципе, removePunctuation() - это функция общего назначения в том смысле, что она предназначена для работы с любыми текстами вне зависимости от их специфических особенностей (например, наличие аббревиатур, смайликов, номеров телефонов, и т.п.). Она, по-видимому, хорошо подойдет для разведочного анализа. Однако для полноценной обработки конкретного корпуса я бы писал свою функцию с ипользованием регулярных выражений, учитывая результаты разведочного анализа (т.е., зная, что НЕ срабатывает при использовании готовых функций).

Анонимный комментирует...

Сергей, всегда рад помочь:)
Ещё забыл упомянуть о таком наблюдении: при выполнении функции removeWords(), чистка текста от шумовых слов происходит последовательно по списку стоп-слов. Из за того, что есть глюк с буквами "ё" и "ч", слова содержащие эти буквы могут быть очищены не корректно.

Приведу пример:
> removeWords("Он очень ждал встречи с ней", c("о", "очень"))
[1] "Он чень ждал встречи с ней"
> removeWords("Он очень ждал встречи с ней", c("очень", "о"))
[1] "Он ждал встречи с ней"

Как видите, в первом случае из слова "очень" выпала только буква "о" - в результате образовалось некорректное слово. Во втором случае, слово "очень" было корректно удалено из текста.
Очевидно, на функцию влияет порядок стоп-слов. Поэтому, имеет смысл упорядочивать список со словами по убыванию количества символов. Тогда из текста будут, сначала, удалятся более специфические (длинные) слова, а затем более простые и односложные.
Примерно так: MyStopWord[rev(order(nchar(MyStopWord)))]

Вполне может быть, что проблема имеет место и с другими словами, но я пока их не обнаружил.
В общем, стоит быть внимательным с пакетом "tm". И Вы правы - лучше использовать регулярные выражения, и самому корректировать особенности чистки.

Анонимный комментирует...

Сергей,
спешу поделится радостью. Кажется удалось разобраться с проблемой выпадания букв при работе с русскоязычным текстом. Очевидно проблема в кодировке - нужно преобразовывать текстовые строки в кодировку UTF-8. Оказывается, для этого есть отдельная функция enc2utf8(). Подробнее можно почитать при запросе: ?Encoding

Пытался задать преобразование объекта в UTF-8 на этапе чтения файла .csv - почему-то не получилось, строки превращаются в набор бессмысленных символов. Если будете разбираться с этим вопросом - напишите пожалуйста, как должен выглядеть набор аргументов при чтении файла.

Sergey Mastitsky комментирует...

Спасибо за дополнительную информацию. Да, как и предполагалось, проблема оказалась в кодировке. Кстати, будьте внимательны - функции пакета tm часто "сбивают" кодировку, так что приходится переодировать весь корпус снова и снова после определенных операций.
У меня к вам предложение: не хотели бы Вы написать небольшое гостевой пост по этой теме, который включил бы описание проблем, с которыми Вы столкнулись, и как получилось их решить? Если Вам это интересно, пишите в "личку" (адрес можно найти на странице "Обо мне").

Olga комментирует...

Сергей, добрый день! Нашла этот пост через гугл.
Не могу разобраться вот с чем: если писать в searchstring запрос кириллицей, то в результате выдаются последние твиты согласно другим условиям, но без собственно слова-запроса.

Например по запросу searchTwitter(searchString="НБУ", lang="uk", n = 3)
Выдается:
[[1]] [1] "sparzicin: решебник ефимов демидович часть 1: решебник ефимов демидович часть 1"
[[2]] [1] "anna_ponomaren: Шахтар - Фенербахче, вболіваймо за наших!!"
[[3]] [1] "anthonyeyepecak: Реформа прокуратуры глазами юристов"

Сталкивались ли вы с таким, как это можно исправить? В сети вижу примеры с кириллицей только на Java, но не в R

Заранее спасибо!

Sergey Mastitsky комментирует...

Ольга, сложно сказать, в чем может быть дело - локаль вашей машины? кодировка кириллического текста?
C кириллицей почти всегда что-то не так, к сожалению, и решения обычно находятся (если находятся) не очевидные. Может, здесь что-то найдете полезное: http://r-analytics.blogspot.co.uk/2014/11/r.html

Olga комментирует...

Сергей, спасибо огромное за оперативный ответ!
Действительно, пост по ссылке помог: если сделать запрос вот так: searchTwitter(searchString=enc2utf8("НБУ"), lang="uk", n = 3)
то ищется правильно )

Maxim Shevchenko комментирует...

Почему то на 2 последние строчки кода ругается(

Maxim Shevchenko комментирует...

Подскажите пожалуйста что за ошибка: набрал Ваш код, после строчки - print(generateWordcloud(my.corpus), 15) - выдал ошибку Error: inherits(doc, "TextDocument") не TRUE. Не знаю что это

Максим Спирихин комментирует...

Maxim, введите вот это перед этим кодом - мне помогло
my.corpus <- tm_map(my.corpus, PlainTextDocument)

Отправить комментарий