In this exercise, we’re going to explore what makes news stories go viral. The data we’ll use is from the New York Times facebook page. At the end of the session, we’ll ask you to present your results to the class very briefly (2-3 min) and casually (just the html output of this Rmd file will do). Bolded items below should be part of your presentation!
% Note: the ‘eval=FALSE’ argument below prevents the R code from being run when you compile % the RMarkdown document, since it’s slow
library(Rfacebook)
# check the help for the function fbOauth (type into R console ?fbOAuth)
# for instructions on obtaining a temporary access token
# when you have a token, paste it in place of abc123" below
auth_token <- "abc123"
nyt.fb <- getPage(page="nytimes", token=auth_token, n=100, since="2015/01/01")
nyt.fb <- read.csv("./nyt-fb.csv", stringsAsFactors=FALSE)
str(nyt.fb)
## 'data.frame': 9960 obs. of 10 variables:
## $ from_id : num 5.28e+09 5.28e+09 5.28e+09 5.28e+09 5.28e+09 ...
## $ from_name : chr "The New York Times" "The New York Times" "The New York Times" "The New York Times" ...
## $ message : chr "\"What if our toxic pattern was how well we got along and how much we loved each other?\"" "If you're on the East Coast, here are The New York Times Food's delicious ways to hunker down before the storm rolls in." "“At the time when I came into this country, they didn’t give us money or anything like that. You came in. You put your money in"| __truncated__ "\"It's OK, we have a really great water, from Australia.\"" ...
## $ created_time : chr "2016-01-22T00:25:00+0000" "2016-01-21T23:55:00+0000" "2016-01-21T23:25:00+0000" "2016-01-21T22:55:00+0000" ...
## $ type : chr "link" "link" "photo" "link" ...
## $ link : chr "http://nyti.ms/1P8WA8w" "http://cooking.nytimes.com/68861692-nyt-cooking/922671-cooking-for-the-storm?smid=fb-nytimes&smtyp=cur" "https://www.facebook.com/nytimes/photos/a.283559809998.33779.5281959998/10150729101899999/?type=3" "http://nyti.ms/1OIM2th" ...
## $ id : chr "5281959998_10150730340399999" "5281959998_10150730161409999" "5281959998_10150729101899999" "5281959998_10150730302924999" ...
## $ likes_count : int 128 438 106 478 322 481 293 4774 190 228 ...
## $ comments_count: int 8 32 36 46 161 22 28 312 28 35 ...
## $ shares_count : int 24 202 20 98 76 91 204 1280 50 49 ...
month <- substr(nyt.fb$created_time, 6, 7)
hour <- substr(nyt.fb$created_time, 12, 13)
nyt.fb <- data.frame(nyt.fb, month, hour)
# write.csv(data.frame(month, hour), file="month-hour.csv", row.names=F)
viral based on this and add it to your data frame.total.resp <- nyt.fb$likes_count + nyt.fb$shares_count + nyt.fb$comments_count
quantile(total.resp, .95) # top 5% total share/count/comment total
## 95%
## 10928.4
viral <- total.resp > 10000
nyt.fb$viral <- viral
# write.csv(data.frame(viral), file="viral.csv", row.names=F)
nyt.fb data frame. Note that you will need to preprocess the text first (removing punctuation, converting to lowercase, etc.). Can you interpret the topics based on their associated terms?library(topicmodels)
library(tm)
## Loading required package: NLP
# nyt.fb$message <- gsub('\\"', "", x=nyt.fb$message)
corpus <- VCorpus(VectorSource(nyt.fb$message))
corpus <- tm_map(corpus, removePunctuation)
corpus <- tm_map(corpus, content_transformer(tolower))
corpus <- tm_map(corpus, function(x) removeWords(x, stopwords("english")))
dtm <- DocumentTermMatrix(corpus, control=list(bounds=list(global=c(5, Inf))))
# remove messages with no topic terms
row.sum <- apply(dtm , 1, sum)
dtm.new <- dtm[row.sum > 0, ]
# fit topic model, k=20 topics
lda <- LDA(dtm.new, k=20, method="Gibbs",
control=list(verbose=1000, seed=0, burnin=100, iter=5000))
## K = 20; V = 4974; M = 9960
## Sampling 5100 iterations!
## Iteration 1000 ...
## Iteration 2000 ...
## Iteration 3000 ...
## Iteration 4000 ...
## Iteration 5000 ...
## Gibbs sampling completed!
terms <- get_terms(lda, 10)
terms # look pretty interpretable
## Topic 1 Topic 2 Topic 3 Topic 4 Topic 5 Topic 6
## [1,] "president" "new" "world" "now" "news" "said"
## [2,] "obama" "york" "first" "school" "breaking" "just"
## [3,] "barack" "times" "time" "children" "said" "get"
## [4,] "house" "opinion" "city" "work" "two" "good"
## [5,] "white" "read" "game" "money" "officials" "going"
## [6,] "americans" "section" "series" "come" "federal" "something"
## [7,] "john" "writes" "major" "even" "found" "people"
## [8,] "deal" "board" "sports" "parents" "government" "really"
## [9,] "american" "editorial" "team" "less" "case" "like"
## [10,] "will" "nicholas" "since" "told" "including" "things"
## Topic 7 Topic 8 Topic 9 Topic 10 Topic 11 Topic 12
## [1,] "day" "change" "say" "donald" "like" "state"
## [2,] "back" "health" "women" "trump" "know" "paris"
## [3,] "life" "court" "men" "hillary" "want" "attacks"
## [4,] "way" "said" "part" "clinton" "dont" "isis"
## [5,] "time" "right" "made" "republican" "need" "across"
## [6,] "home" "francis" "young" "campaign" "love" "war"
## [7,] "every" "pope" "theres" "debate" "well" "islamic"
## [8,] "quote" "care" "often" "bernie" "youre" "trying"
## [9,] "long" "marriage" "woman" "politics" "didnt" "friday"
## [10,] "making" "law" "said" "presidential" "story" "europe"
## Topic 13 Topic 14 Topic 15 Topic 16 Topic 17 Topic 18
## [1,] "look" "american" "years" "people" "new" "year"
## [2,] "think" "states" "now" "police" "york" "last"
## [3,] "week" "united" "best" "man" "times" "show"
## [4,] "ahead" "million" "watch" "black" "via" "photo"
## [5,] "today" "nearly" "still" "death" "food" "live"
## [6,] "take" "since" "ago" "killed" "photo" "photos"
## [7,] "national" "china" "big" "least" "styles" "david"
## [8,] "season" "plan" "next" "video" "summer" "follow"
## [9,] "spoilers" "nations" "star" "officers" "dining" "see"
## [10,] "might" "used" "far" "south" "fashion" "sunday"
## Topic 19 Topic 20
## [1,] "one" "will"
## [2,] "never" "can"
## [3,] "around" "many"
## [4,] "ever" "may"
## [5,] "history" "make"
## [6,] "took" "help"
## [7,] "much" "use"
## [8,] "2015" "without"
## [9,] "another" "better"
## [10,] "place" "much"
topics <- get_topics(lda, 3)
topic1 <- as.character(topics[1, ])
topic2 <- as.character(topics[2, ])
topic3 <- as.character(topics[3, ])
nyt.fb$topic1 <- topic1
nyt.fb$topic2 <- topic2
nyt.fb$topic3 <- topic3
# write.csv(data.frame(topic1, topic2, topic3), file="topics.csv", row.names=F)
nyt.fb data frame. It will be helpful to use regular expressions to match the key words as flexibly as possible.message <- removePunctuation(tolower(nyt.fb$message))
nyt.fb$israel <- grepl("israel", message)
nyt.fb$trump <- grepl("trump", message)
nyt.fb$hillary <- grepl("hillary", message)
nyt.fb$obama <- grepl("barack|obama", message)
nyt.fb$terror <- grepl("terror|isis|isil|qaeda", message)
nyt.fb$kill <- grepl("kill|murder|shot", message)
nyt.fb$debate <- grepl("debat", message)
# write.csv(nyt.fb[, c("israel", "trump", "hillary", "obama", "terror", "kill", "debate")], file="keywords.csv", row.names=F)
lexicon.csv we saw in the “text” module, extract a sentiment score for the content of each message. Add this score to the nyt.fb data frame.lexicon <- read.csv("lexicon.csv", stringsAsFactors=F)
pos.words <- lexicon$word[lexicon$polarity=="positive"]
neg.words <- lexicon$word[lexicon$polarity=="negative"]
get_sentiment <- function(message, pos.words, neg.words){
# count number of positive and negative word matches in message
words.list <- strsplit(message, " +")
pos.matches <- sapply(words.list, function(w) sum(w %in% pos.words))
neg.matches <- sapply(words.list, function(w) sum(w %in% neg.words))
return(pos.matches - neg.matches)
}
message <- removePunctuation(tolower(nyt.fb$message))
sent.score <- get_sentiment(message, pos.words, neg.words)
nyt.fb$sent.score <- sent.score
# write.csv(data.frame(sent.score), file="sent-score.csv", row.names=F)
viral variable), and then classify “viral” or not based on the predictions of that model.# SVM example
library(e1071)
svm.viral <- svm(as.factor(viral) ~ month + hour + topic1 + topic2 + topic3 +
israel + trump + hillary + obama + terror + kill +
debate + sent.score, data=nyt.fb)
# use log of total response count
svm.resp <- svm(I(log(shares_count + likes_count + comments_count)) ~ month +
hour + topic1 + topic2 + topic3 + israel + trump +
hillary + obama + terror + kill + debate + sent.score, data=nyt.fb)
# regression example
glm.viral <- glm(as.factor(viral) ~ month + hour + topic1 + topic2 + topic3 +
israel + trump + hillary + obama + terror + kill +
debate + sent.score, data=nyt.fb, family=binomial(logit))
lm.resp <- lm(I(log(shares_count + likes_count + comments_count)) ~ month +
hour + topic1 + topic2 + topic3 + israel + trump +
hillary + obama + terror + kill + debate + sent.score, data=nyt.fb)
# random forest example
library(randomForest)
## randomForest 4.6-12
## Type rfNews() to see new features/changes/bug fixes.
rf.viral <- randomForest(as.factor(viral) ~ month + hour + topic1 + topic2 + topic3 +
israel + trump + hillary + obama + terror + kill +
debate + sent.score, data=nyt.fb)
rf.resp <- randomForest(I(log(shares_count + likes_count + comments_count)) ~ month +
hour + topic1 + topic2 + topic3 + israel + trump +
hillary + obama + terror + kill + debate + sent.score, data=nyt.fb)
set.seed(0)
train.sample <- sample(nrow(nyt.fb), floor(.75 * nrow(nyt.fb)))
train.data <- nyt.fb[train.sample, ]
test.data <- nyt.fb[-train.sample, ]
svm.resp <- svm(I(log(shares_count + likes_count + comments_count)) ~ month +
hour + topic1 + topic2 + topic3 + israel + trump +
hillary + obama + terror + kill + debate + sent.score, data=train.data)
lm.resp <- lm(I(log(shares_count + likes_count + comments_count)) ~ month +
hour + topic1 + topic2 + topic3 + israel + trump +
hillary + obama + terror + kill + debate + sent.score, data=train.data)
rf.resp <- randomForest(I(log(shares_count + likes_count + comments_count)) ~ month +
hour + topic1 + topic2 + topic3 + israel + trump +
hillary + obama + terror + kill + debate + sent.score, data=train.data)
# get predictions on test data
svm.resp.pred <- predict(svm.resp, test.data)
lm.resp.pred <- predict(lm.resp, test.data)
rf.resp.pred <- predict(rf.resp, test.data)
resp_to_viral <- function(resp, cutoff=10000, logged=TRUE) {
if(logged)
resp <- exp(resp)
viral <- resp > cutoff
return(viral)
}
# set cutoff so that % predicted viral in test data is the same as in training data
svm.viral.pred <- resp_to_viral(svm.resp.pred, cutoff=2250)
mean(svm.viral.pred)
## [1] 0.05100402
lm.viral.pred <- resp_to_viral(lm.resp.pred, cutoff=2450)
rf.viral.pred <- resp_to_viral(rf.resp.pred, cutoff=2480)
# evaluate predictions
library(caret)
## Loading required package: lattice
## Loading required package: ggplot2
##
## Attaching package: 'ggplot2'
##
## The following object is masked from 'package:randomForest':
##
## margin
##
## The following object is masked from 'package:NLP':
##
## annotate
confusionMatrix(test.data$viral, svm.viral.pred)
## Confusion Matrix and Statistics
##
## Reference
## Prediction FALSE TRUE
## FALSE 2250 115
## TRUE 113 12
##
## Accuracy : 0.9084
## 95% CI : (0.8964, 0.9195)
## No Information Rate : 0.949
## P-Value [Acc > NIR] : 1.0000
##
## Kappa : 0.047
## Mcnemar's Test P-Value : 0.9472
##
## Sensitivity : 0.95218
## Specificity : 0.09449
## Pos Pred Value : 0.95137
## Neg Pred Value : 0.09600
## Prevalence : 0.94900
## Detection Rate : 0.90361
## Detection Prevalence : 0.94980
## Balanced Accuracy : 0.52333
##
## 'Positive' Class : FALSE
##
confusionMatrix(test.data$viral, lm.viral.pred)
## Confusion Matrix and Statistics
##
## Reference
## Prediction FALSE TRUE
## FALSE 2231 134
## TRUE 102 23
##
## Accuracy : 0.9052
## 95% CI : (0.893, 0.9164)
## No Information Rate : 0.9369
## P-Value [Acc > NIR] : 1.0000
##
## Kappa : 0.1136
## Mcnemar's Test P-Value : 0.0436
##
## Sensitivity : 0.9563
## Specificity : 0.1465
## Pos Pred Value : 0.9433
## Neg Pred Value : 0.1840
## Prevalence : 0.9369
## Detection Rate : 0.8960
## Detection Prevalence : 0.9498
## Balanced Accuracy : 0.5514
##
## 'Positive' Class : FALSE
##
confusionMatrix(test.data$viral, rf.viral.pred)
## Confusion Matrix and Statistics
##
## Reference
## Prediction FALSE TRUE
## FALSE 2245 120
## TRUE 107 18
##
## Accuracy : 0.9088
## 95% CI : (0.8968, 0.9199)
## No Information Rate : 0.9446
## P-Value [Acc > NIR] : 1.0000
##
## Kappa : 0.0889
## Mcnemar's Test P-Value : 0.4258
##
## Sensitivity : 0.9545
## Specificity : 0.1304
## Pos Pred Value : 0.9493
## Neg Pred Value : 0.1440
## Prevalence : 0.9446
## Detection Rate : 0.9016
## Detection Prevalence : 0.9498
## Balanced Accuracy : 0.5425
##
## 'Positive' Class : FALSE
##
correct.pred <- sample(which(test.data$viral & svm.viral.pred), 1)
incorrect.pred <- sample(which(!test.data$viral & svm.viral.pred), 1)
test.data[correct.pred, ]
## from_id from_name
## 2857 5281959998 The New York Times
## message
## 2857 "We are reminded in this time of tragedy that the bonds of liberté and égalité and fraternité are not only values that the French people care so deeply about, but they are values that we share. And those values are going to endure far beyond any act of terrorism or the hateful vision of those who perpetrated the crimes this evening." — President Barack Obama
## created_time type
## 2857 2015-11-14T03:06:12+0000 video
## link
## 2857 https://www.facebook.com/nytimes/videos/10150694294884999/
## id likes_count comments_count shares_count
## 2857 5281959998_10150694294884999 20063 903 2172
## month hour viral topic1 topic2 topic3 israel trump hillary obama
## 2857 11 03 TRUE 8 1 11 FALSE FALSE FALSE TRUE
## terror kill debate sent.score
## 2857 TRUE FALSE FALSE 0
test.data[incorrect.pred, ]
## from_id from_name
## 6835 5281959998 The New York Times
## message
## 6835 From instagram.com/nytimesfashion: Dominique Thaysen of Cape Town feeds a carrot to one of the tortoises at the Turtle Conservancy in Ojai, Southern California.\n\nPhoto: Jake Michaels for The New York Times
## created_time type
## 6835 2015-07-13T23:25:01+0000 photo
## link
## 6835 https://www.facebook.com/nytimes/photos/a.283559809998.33779.5281959998/10150624412599999/?type=3
## id likes_count comments_count shares_count
## 6835 5281959998_10150624412599999 2341 49 135
## month hour viral topic1 topic2 topic3 israel trump hillary obama
## 6835 07 23 FALSE 17 2 4 FALSE FALSE FALSE FALSE
## terror kill debate sent.score
## 6835 FALSE FALSE FALSE 0