--- title: "Sentiment Analysis of Twitter Data" date: "`r Sys.Date()`" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{saotd} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r setup, include=FALSE} knitr::opts_chunk$set(echo = TRUE, eval = TRUE) # eval = requireNamespace("rtweet", quietly = TRUE)) options(rmarkdown.html_vignette.check_title = FALSE) ``` # Sentiment Analysis of Twitter Data (saotd) ## Twitter Introduction Recent years have witnessed the rapid growth of social media platforms in which users can publish their individual thoughts and opinions (e.g., Facebook, Twitter, Google+ and several blogs). The rise in popularity of social media has changed the world wide web from a static repository to a dynamic forum for anyone to voice their opinion across the globe. This new dimension of _User Generated Content_ opens up a new and dynamic source of insight to individuals, organizations and governments. Social network sites or platforms, are defined as web-based services that allow individuals to: * Construct a public or semi-public profile within a bounded system. * Articulate a list of other users with whom they share a connection. * View and traverse their list of connections and those made by others within the system. The nature and nomenclature of these connections may vary from site to site. This package, `saotd` is focused on utilizing Twitter data due to its widespread global acceptance. Harvested data, analyzed for sentiment can provide powerful insight into a population. This insight can assist organizations, by letting them better understand their target population. This package will allow a user to acquire data using the Public Twitter Application Programming Interface (API), to obtain tweets. The `saotd` package is broken down into five different phases: * Acquire * Explore * Topic Analysis * Sentiment Calculation * Visualization The `saotd` package workflow can be observed referenced via the below image that will take and analysis from the Twitter API to through a complete analysis. ```{r workflow, echo=FALSE, out.width="100%"} knitr::include_graphics(path = "saotd_workflow.png") ``` ## Packages ```{r packages, warning=FALSE, message=FALSE} library(saotd) library(dplyr) library(stringr) library(knitr) ``` ## Acquire To explore the data manipulation functions of `saotd` we will use the built in dataset `saotd::raw_tweets`. However is you want to acquire your own tweets, you will first have to: 1. Create a [twitter](https://twitter.com/) account or sign into existing account. 2. Use your twitter login, to sign into [Twitter Developers](https://developer.twitter.com/apps) 3. Navigate to My Applications. 4. Fill out the new application form. + You will be asked to provide a website. + You can input your twitter account website. + For example: https://twitter.com/yourusername 5. Create access token. + Record twitter access keys and tokens With these steps complete you now have access to the twitter API. To acquire your own dataset of tweets you can use the `saotd::tweet_acquire` function and insert your consumer key, consumer secret key, access token and access secret key gained from the [Twitter Developers](https://developer.twitter.com/apps) page. You will also need to select the #hashtags you are interested in and the number of tweets requested per #hashtag. ```{r tweet_acquire, echo=TRUE, eval=FALSE, cache=TRUE, cache.path='saotd_cache/'} consumer_api_key <- "XXXXXXXXXXXXXXXXXXXXXXXXX" consumer_api_secret_key <- "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" access_token <- "XXXXXXXXXXXXXXXXXX-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" access_token_secret <- "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" hashtags <- c("#job", "#Friday", "#fail", "#icecream", "#random", "#kitten", "#airline") tweets <- tweet_acquire( twitter_app = "twitter_app", consumer_api_key = Sys.getenv('consumer_api_key'), consumer_api_secret_key = Sys.getenv('consumer_api_secret_key'), access_token = Sys.getenv('access_token'), access_token_secret = Sys.getenv('access_token_secret'), query = "#icecream", num_tweets = 100, distinct = TRUE) ``` ## Explore You can acquire your own data or use the dataset included with the package. We will be using the included data `raw_tweets`. This dataset was acquired from a [Twitter US Airline Sentiment]() Kaggle competition, from December 2017. The dataset contains 14,487 tweets from 6 different hashtags (2,604 x #American, 2,220 x #Delta, 2,420 x #Southwest, 3,822 x #United, 2,913 x #US Airways, 504 x #Virgin America). ```{r raw_tweets, cache=TRUE, cache.path='saotd_cache/'} set.seed(4321) data("raw_tweets") TD <- raw_tweets %>% dplyr::sample_n(size = 5000, replace = TRUE) ``` The first tweet of the dataset is: "@SouthwestAir I filled in the form on the website too. Darn it all. I guess I'll just have to cross my fingers.", and when it is cleaned and tidy'd it becomes: ```{r tidy, cache=TRUE, cache.path='saotd_cache/'} TD_Tidy <- saotd::tweet_tidy( DataFrame = TD) TD_Tidy$Token[1:9] %>% knitr::kable("html") ``` The cleaning process removes: "@", "#" and "RT" symbols, Weblinks, Punctuation, Emojis, and Stop Words like ("the", "of", etc.). We will now investigate Uni-Grams, Bi-Grams and Tri-Grams. ```{r unigram, cache=TRUE, cache.path='saotd_cache/'} saotd::unigram(DataFrame = TD) %>% dplyr::top_n(10) %>% knitr::kable("html", caption = "Twitter data Uni-Grams") ``` ```{r bigram, message=FALSE, cache=TRUE, cache.path='saotd_cache/'} saotd::bigram(DataFrame = TD) %>% dplyr::top_n(10) %>% knitr::kable("html", caption = "Twitter data Bi-Grams") ``` ```{r trigram, message=FALSE, cache=TRUE, cache.path='saotd_cache/'} saotd::trigram(DataFrame = TD) %>% dplyr::top_n(10) %>% knitr::kable("html", caption = "Twitter data Tri-Grams") ``` Now that we have the Uni-Grams we can see that canceled and flight are referring to canceled flight and may be good set of words to merge into a single term. Additionally, pet and pets could also be merged to observe more uniqueness in the data. ```{r merge, message=FALSE, message=FALSE, error=FALSE, cache=TRUE, cache.path='saotd_cache/'} TD_Merge <- merge_terms( DataFrame = TD, term = "cancelled flight", term_replacement = "cancelled_flight") ``` Now that the terms have been merged, the new N-Grams are re-computed. ```{r merged_unigram, message=FALSE, cache=TRUE, cache.path='saotd_cache/'} saotd::unigram(DataFrame = TD_Merge) %>% dplyr::top_n(10) %>% knitr::kable("html", caption = "Twitter data Uni-Grams") ``` ```{r merged_bigram, message=FALSE, cache=TRUE, cache.path='saotd_cache/'} saotd::bigram(DataFrame = TD_Merge) %>% dplyr::top_n(10) %>% knitr::kable("html", caption = "Twitter data Bi-Grams") ``` ```{r merged_trigram, message=FALSE, cache=TRUE, cache.path='saotd_cache/'} saotd::trigram(DataFrame = TD_Merge) %>% dplyr::top_n(10) %>% knitr::kable("html", caption = "Twitter data Tri-Grams") ``` Now we can look at Bi-Gram Networks. ```{r bigram_network, fig.align='center', cache=TRUE, cache.path='saotd_cache/'} TD_Bigram <- saotd::bigram(DataFrame = TD_Merge) saotd::bigram_network( BiGramDataFrame = TD_Bigram, number = 30, layout = "fr", edge_color = "blue", node_color = "black", node_size = 3, set_seed = 1234) ``` Additionally we can observe the Correlation Network. ```{r corr_network, fig.align='center', cache=TRUE, cache.path='saotd_cache/'} TD_Corr <- saotd::word_corr( DataFrameTidy = TD_Tidy, number = 100, sort = TRUE) saotd::word_corr_network( WordCorr = TD_Corr, Correlation = .1, layout = "fr", edge_color = "blue", node_color = "black", node_size = 1) ``` ## Sentiment Calculation Now that the data has been explored we will need to compute the Sentiment scores for the hashtags. ```{r scores, cache=TRUE, cache.path='saotd_cache/'} TD_Scores <- saotd::tweet_scores( DataFrameTidy = TD_Tidy, HT_Topic = "hashtag") ``` With the scores computed we can then observe the positive and negative words within the dataset. ```{r posneg_words, fig.align='center', cache=TRUE, cache.path='saotd_cache/'} saotd::posneg_words( DataFrameTidy = TD_Tidy, num_words = 10) ``` As an example we can see that the negative term "fail" is dwarfing all other responses. If we would like to remove "fail" we can easily do it. ```{r filtered_posneg_words, fig.align='center', cache=TRUE, cache.path='saotd_cache/'} saotd::posneg_words( DataFrameTidy = TD_Tidy, num_words = 10, filterword = "fail") ``` We can see the most positive tweets hashtags within the the data set. ```{r max_scores, cache=TRUE, cache.path='saotd_cache/'} saotd::tweet_max_scores( DataFrameTidyScores = TD_Scores, HT_Topic = "hashtag") ``` We can also see the most negative hashtag tweets within the data set. ```{r min_scores, cache=TRUE, cache.path='saotd_cache/'} saotd::tweet_min_scores( DataFrameTidyScores = TD_Scores, HT_Topic = "hashtag") ``` Furthermore if we wanted to observe the most positive or negative hashtag scores associated with a specific hashtag we could also do that. ```{r filtered_max_scores, cache=TRUE, cache.path='saotd_cache/'} saotd::tweet_max_scores( DataFrameTidyScores = TD_Scores, HT_Topic = "hashtag", HT_Topic_Selection = "United") ``` ## Topic Analysis If we were interested in conducting a topic analysis on the tweets we would then determine the number of latent topics within the tweet data. ```{r number_topics_plot, eval=FALSE, fig.align='center', warning=FALSE, message=FALSE, results="hide", cache=TRUE, cache.path='saotd_cache/'} saotd::number_topics( DataFrame = TD, num_cores = 4L, min_clusters = 2, max_clusters = 12, skip = 1, set_seed = 1234) ``` ```{r lda_tuning_plot, echo=FALSE, out.width="100%"} knitr::include_graphics(path = "lda_topics.png") ``` The number of topics plot shows that between 5 and 7 latent topics reside within the dataset. For this example we could select between 5 and 7 topics to categorize this data. In this case 5 topics will be selected to continue the analysis. ```{r topics, fig.align='center', warning=FALSE, message=FALSE, results='hide', cache=TRUE, cache.path='saotd_cache/'} TD_Topics <- saotd::tweet_topics( DataFrame = TD, clusters = 5, method = "Gibbs", set_seed = 1234, num_terms = 10) ``` In a markdown product the topics table does not print clearly, unlike when it is printed in the console. However the words associated with each topic can be observed in the below table. |Number | Topic 1 | Topic 2 | Topic 3 | Topic 4 | Topic 5 | |:-------:|:-------:|:--------:|:----------:|:------------:|:--------:| |1 |united |usairways |americanair |southwestair |flight | |2 |service |time |usairways |jetblue |cancelled | |3 |customer |plane |amp |im |hours | |4 |dont |gate |hold |virginamerica |flights | |5 |bag |jetblue |call |guys |2 | |6 |check |hour |phone |fly |delayed | |7 |luggage |waiting |wait |airline |flightled | |8 |dm |delay |ive |flying |late | |9 |lost |people |cange |seat |3 | |10 |worst |minutes |day |love |weather | One of the challenges of using a topic model is selecting the correct number of topics. As we can see in the above chart. We went from 6 hashtags to 5 different topics. While this may not be the best example to use, we will continue the topic modeling example. We would first want to rename the topics into something that would make sense. In this case Topic 1 could be luggage, Topic 2 could be delay, Topic 3 could be customer_service, Topic 4 could be enjoy, and Topic 5 could be delay These topics were chosen by observing the words associated with each topic. This selection could be different depending on experience and a deeper understanding of the topics. We would then want to rename the topics in the dataframe ```{r rename_topics, cache=TRUE, cache.path='saotd_cache/'} TD_Topics <- TD_Topics %>% dplyr::mutate(Topic = stringr::str_replace_all(Topic, "^1$", "luggage")) %>% dplyr::mutate(Topic = stringr::str_replace_all(Topic, "^2$", "gate_delay")) %>% dplyr::mutate(Topic = stringr::str_replace_all(Topic, "^3$", "customer_service")) %>% dplyr::mutate(Topic = stringr::str_replace_all(Topic, "^4$", "enjoy")) %>% dplyr::mutate(Topic = stringr::str_replace_all(Topic, "^5$", "other_delay")) ``` Next we would want to tidy and then score the new topic dataset. ```{r topic_tidy, cache=TRUE, cache.path='saotd_cache/'} TD_Topics_Tidy <- saotd::tweet_tidy( DataFrame = TD_Topics) TD_Topics_Scores <- saotd::tweet_scores( DataFrameTidy = TD_Topics_Tidy, HT_Topic = "topic") ``` We can see the most positive topic tweets within the data set. ```{r topic_max_scores, cache=TRUE, cache.path='saotd_cache/'} saotd::tweet_max_scores( DataFrameTidyScores = TD_Topics_Scores, HT_Topic = "topic") ``` We can also see the most negative topics tweets within the data set. ```{r topic_min_scores, cache=TRUE, cache.path='saotd_cache/'} saotd::tweet_min_scores( DataFrameTidyScores = TD_Topics_Scores, HT_Topic = "topic") ``` Furthermore if we wanted to observe the most positive or negative scores associated with a specific topic we could also do that. ```{r topic_filtered_max_scores, cache=TRUE, cache.path='saotd_cache/'} saotd::tweet_max_scores( DataFrameTidyScores = TD_Topics_Scores, HT_Topic = "topic", HT_Topic_Selection = "luggage") ``` ## Visualizations ### Hashtags Now we will begin visualizing the hashtag data. The distribution of the sentiment scores can be found in the below plot. ```{r corpus_distribution, fig.align='center', cache=TRUE, cache.path='saotd_cache/'} saotd::tweet_corpus_distribution( DataFrameTidyScores = TD_Scores, color = "black", fill = "white") ``` Additionally if we wanted to see the score distributions per each hashtag, we can find it below. ```{r tweet_distribution, fig.align='center', cache=TRUE, cache.path='saotd_cache/'} saotd::tweet_distribution( DataFrameTidyScores = TD_Scores, HT_Topic = "hashtag", bin_width = 1, color = "black", fill = "white") ``` We can also observe the hashtag distributions as a Box plot. ```{r box_plot, fig.align='center', cache=TRUE, cache.path='saotd_cache/'} saotd::tweet_box( DataFrameTidyScores = TD_Scores, HT_Topic = "hashtag") ``` Also as a Violin plot. The chevrons in each violin plot denote the median of the data and provide a quick reference point to see if a hashtag is generally positive or negative. For example the "random" hashtag has a generally negative sentiment, where as the "kitten" hashtags has a generally positive sentiment. ```{r violin_plot, fig.align='center', cache=TRUE, cache.path='saotd_cache/'} saotd::tweet_violin( DataFrameTidyScores = TD_Scores, HT_Topic = "hashtag") ``` One of the more interesting ways to visualize the Twitter data is to observe the change in sentiment over time. This dataset was acquired on a single day and therefore some of the hashtags did not overlap days. However some did and we can see the change in sentiment scores through time. ```{r time_plot, fig.align='center', warning=FALSE, message=FALSE, cache=TRUE, cache.path='saotd_cache/'} saotd::tweet_time( DataFrameTidyScores = TD_Scores, HT_Topic = "hashtag") ``` Finally if a Twitter user has not disabled georeferencing data the location of the tweet can be observed. However in many cases this may not be very insightful because of the lack of data. ### Topics Now we will begin visualizing the topic data. The distribution of the sentiment scores can be found in the below plot. ```{r topic_corpus_distribution, fig.align='center', cache=TRUE, cache.path='saotd_cache/'} saotd::tweet_corpus_distribution( DataFrameTidyScores = TD_Topics_Scores, color = "black", fill = "white") ``` Additionally if we wanted to see the score distributions per each topic, we can find it below. ```{r topic_tweet_distribution, fig.align='center', cache=TRUE, cache.path='saotd_cache/'} saotd::tweet_distribution( DataFrameTidyScores = TD_Topics_Scores, HT_Topic = "topic", bin_width = 1, color = "black", fill = "white") ``` We can also observe the topic distributions as a Box plot. ```{r topic_box_plot, fig.align='center', cache=TRUE, cache.path='saotd_cache/'} saotd::tweet_box( DataFrameTidyScores = TD_Topics_Scores, HT_Topic = "topic") ``` Also as a Violin plot. The chevrons in each violin plot denote the median of the data and provide a quick reference point to see if a hashtag is generally positive or negative. For example the "random" hashtag has a generally negative sentiment, where as the "kitten" hashtags has a generally positive sentiment. ```{r topic_violin_plot, fig.align='center', cache=TRUE, cache.path='saotd_cache/'} saotd::tweet_violin( DataFrameTidyScores = TD_Topics_Scores, HT_Topic = "topic") ``` One of the more interesting ways to visualize the Twitter data is to observe the change in sentiment over time. This dataset was acquired on a single day and therefore some of the hashtags did not overlap days. However some did and we can see the change in sentiment scores through time. ```{r topic_time, fig.align='center', warning=FALSE, message=FALSE, cache=TRUE, cache.path='saotd_cache/'} saotd::tweet_time( DataFrameTidyScores = TD_Topics_Scores, HT_Topic = "topic") ```