The data comes from a freely available source here: http://www.dt.fee.unicamp.br/~tiago//youtubespamcollection/
Alberto, T. C., Lochter, J. V., & Almeida, T. A. 2015. TubeSpam: Comment Spam Filtering on YouTube. 2015 IEEE 14th International Conference on Machine Learning and Applications (ICMLA), 138–143. ieeexplore.ieee.org.
We want to build a model that looks at the linguistic properties of the data, and uses it for making predictions. class: 1= spam 0=ham
rm(list = ls())
gc()
## used (Mb) gc trigger (Mb) max used (Mb)
## Ncells 608608 32.6 1392490 74.4 692780 37.0
## Vcells 1150298 8.8 8388608 64.0 1929374 14.8
library(devtools)
## Loading required package: usethis
library(tidyverse)
## ── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ──
## ✔ dplyr 1.1.4 ✔ readr 2.1.5
## ✔ forcats 1.0.0 ✔ stringr 1.5.1
## ✔ ggplot2 3.5.1 ✔ tibble 3.2.1
## ✔ lubridate 1.9.4 ✔ tidyr 1.3.1
## ✔ purrr 1.0.4
## ── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
## ✖ dplyr::filter() masks stats::filter()
## ✖ dplyr::lag() masks stats::lag()
## ℹ Use the conflicted package (<http://conflicted.r-lib.org/>) to force all conflicts to become errors
library(tidytext)
library(quanteda)
## Package version: 4.2.0
## Unicode version: 13.0
## ICU version: 66.1
## Parallel computing: disabled
## See https://quanteda.io for tutorials and examples.
if (!require("quanteda.dictionaries", character.only = TRUE)) {
if (!requireNamespace("remotes", quietly = TRUE)) install.packages("remotes")
remotes::install_github("kbenoit/quanteda.dictionaries")
}
## Loading required package: quanteda.dictionaries
library(quanteda.dictionaries)
Read in and compile each of the individual files that start with
‘Youtube’. Using lapply to bind them all together. Make
column headers lower case. Remove duplicates and missing values. Inspect
the data.
Create tokens.
Regular expressions cheatsheets: https://hypebright.nl/index.php/en/2020/05/25/ultimate-cheatsheet-for-regex-in-r-2/ and https://gist.github.com/shakahl/43443dcba2c8105218350bf14df2c610 are both nice.
The command \w matches word characters, things that are used in
writing typical English words. unnest_tokens will create
tokens (unigrams). New column is called ‘word’, and will contain content
from ‘content’
Text cleaning step!
data("stop_words")
yt %>% unnest_tokens(word, content) %>% #give new column a name - we called it 'word'
count(word, sort=TRUE)
## # A tibble: 4,138 × 2
## word n
## <chr> <int>
## 1 i 636
## 2 this 606
## 3 the 570
## 4 to 498
## 5 a 458
## 6 and 451
## 7 my 412
## 8 you 406
## 9 out 367
## 10 check 364
## # ℹ 4,128 more rows
yt_tokens <- yt %>%
unnest_tokens(word, content) %>% # create the tokens
mutate(word = tolower(word)) %>% # make sure everything is lowercase
anti_join(stop_words) %>% # remove the stop words
filter(nchar(word) >= 2) %>% # remove anything that's short
filter(grepl("\\w+", word)) # delete any punctuation or symbols (words only)
## Joining with `by = join_by(word)`
x <- paste(sort(unique(yt_tokens$word)), collapse = ' ') # unique tokens only sorted - just to have a look
Most common words?
yt_tokens %>%
count(word) %>%
arrange(-n)
## # A tibble: 3,687 × 2
## word n
## <chr> <int>
## 1 check 364
## 2 br 250
## 3 video 238
## 4 song 233
## 5 youtube 198
## 6 love 172
## 7 subscribe 161
## 8 39 156
## 9 http 143
## 10 amp 129
## # ℹ 3,677 more rows
yt_tokens %>% filter(word == "39")
## # A tibble: 156 × 6
## class file id author comment_date word
## <dbl> <chr> <chr> <chr> <dttm> <chr>
## 1 0 Youtube 01-comments Psy.csv z135… Spenc… 2014-11-08 05:29:26 39
## 2 0 Youtube 04-comments KatyPerry.c… z13m… micha… 2014-10-30 05:20:18 39
## 3 1 Youtube 07-comments LMFAO.csv z13t… LaS M… 2015-05-28 19:23:35 39
## 4 1 Youtube 07-comments LMFAO.csv z13t… LaS M… 2015-05-28 19:23:35 39
## 5 0 Youtube 07-comments LMFAO.csv z13y… LoL G… 2015-05-27 13:37:19 39
## 6 0 Youtube 07-comments LMFAO.csv z12s… breat… 2015-05-26 22:21:28 39
## 7 0 Youtube 07-comments LMFAO.csv z12l… goofy… 2015-05-26 20:03:52 39
## 8 0 Youtube 07-comments LMFAO.csv z12s… Like👑… 2015-05-26 16:56:19 39
## 9 0 Youtube 07-comments LMFAO.csv z120… Shock… 2015-05-26 02:03:46 39
## 10 0 Youtube 07-comments LMFAO.csv z120… Shock… 2015-05-26 02:03:46 39
## # ℹ 146 more rows
yt %>% filter(id == "z12sulyo4qmyjvjvm23tznvzfnboetfuj") %>% pull(content)
## [1] "I fuckin love this song!<br /><br /><br />After, I'm sexy and I know it "
Some words are used frequently in every comment, we want to know terms that are used in specific comments.
Compute the TF-IDF for the terms.
word_tfidf <- yt_tokens %>%
count(id, word, sort = T) %>%
bind_tf_idf(word, id, n)
word_tfidf %>%
arrange(-tf_idf)
## # A tibble: 10,461 × 6
## id word n tf idf tf_idf
## <chr> <chr> <int> <dbl> <dbl> <dbl>
## 1 _2viQ_Qnc68fX3dYsfYuM-m4ELMJvxOQBmBOFHqGOk0 beutiful 1 1 7.43 7.43
## 2 z125v3ozoqenvthaz04cdtajsmzwgxkwxug0k superr 1 1 7.43 7.43
## 3 z12agrbxiwabvrb2g22pfpcqys2eyrwbd04 nicei 1 1 7.43 7.43
## 4 z12ai5agdlawczj5x04cfhbr2vuezzvox1s collabo… 1 1 7.43 7.43
## 5 z12bjffg3yu4ybx4e04cjzfbyxqcdzyxl3s hardcore 1 1 7.43 7.43
## 6 z12gcfvpsr3au3vg104cdpw5rvbivre5aeg eminen 1 1 7.43 7.43
## 7 z12jf1h5lznmuhcuw22ruxdyxp2eh32ch likkee 1 1 7.43 7.43
## 8 z12jjrfyukjlw5wyl04cddbo5pysslphbqs0k epic 1 1 7.43 7.43
## 9 z12kstz4owbggv12p230vpcwntjbyrqim04 fit 1 1 7.43 7.43
## 10 z12lcb0rxw3qtlsqm04cebpbjkqjtdza1xs0k telepho… 1 1 7.43 7.43
## # ℹ 10,451 more rows
Number of unique words.
n_distinct(yt_tokens$word)
## [1] 3687
count(yt_tokens, word, sort = T) %>% head(10) # common words
## # A tibble: 10 × 2
## word n
## <chr> <int>
## 1 check 364
## 2 br 250
## 3 video 238
## 4 song 233
## 5 youtube 198
## 6 love 172
## 7 subscribe 161
## 8 39 156
## 9 http 143
## 10 amp 129
count(yt_tokens, word, sort = T) %>% tail(10) # least common words
## # A tibble: 10 × 2
## word n
## <chr> <int>
## 1 comment 1
## 2 damn 1
## 3 ebay 1
## 4 fancy 1
## 5 http 1
## 6 is 1
## 7 shoecollector314 1
## 8 this 1
## 9 usr 1
## 10 www 1
Create test and training sets the same way that they did in the paper. They did it by using the first 70% of each set of comments as the training, and tested on the last 30% of the comments. We can’t do it across all comments since - well, look:
ggplot(yt, aes(x = comment_date, y = file)) +
geom_jitter(alpha = 0.2) + # try comparing with geom_point and/or removing alpha
theme_minimal() # try removing this line (and the +)
The comment dates are very different depending on the video. Look at training and test sets - test set is most recent comments
yt <- yt %>%
group_by(file) %>%
mutate(date_decile = ntile(comment_date, 10)) %>% # break up and divide into 10 even parts based on date
mutate(train = ifelse(date_decile <= 7, 'train', 'test')) %>% # train-test split on each file
ungroup()
ggplot(yt, aes(x = comment_date, y = file, color = train)) + # visualise the split!
geom_jitter(alpha = 0.2) +
theme_minimal()
Need to cast it into a matrix for these next several methods. This is a document term matrix - we talked about them in the lecture - a frequency table of words for each observation.
Its very long - we will make it very wide
https://www.tidytextmining.com/dtm.html
yt_m <- word_tfidf %>%
cast_dtm(id, word, tf_idf) %>% # rows, columns, information inside it
as.matrix
The DTM massive! Don’t try to look at it yet. Lets’ look at the top left corner of this tf-idf term document matrix looks like. (You could calculate a distance matrix from this, and then do some clustering or pca on it…but we won’t)
yt_m[1:5, 1:5]
## Terms
## Docs amp 48051 http
## z12jenlhyre0eheyx04ch1aquxfdsvgpd44 0.55340759 0.000000 0.06238548
## z131idupvn3yhf3mv23dwzhi4pqixvwuw 0.00000000 1.456885 0.54434388
## z132cvvy1ob3ht2er23dundqdtertjmlg 0.05584272 0.000000 0.04406593
## _2viQ_Qnc69MEEHHJxZ427KX8MlljJPnUC2YBbvbWwY 0.00000000 0.000000 0.00000000
## _2viQ_Qnc6_RKHVetk9kLzx8ZC62_J7y73FWFSBTe8Q 0.00000000 0.000000 0.00000000
## Terms
## Docs image2you ru
## z12jenlhyre0eheyx04ch1aquxfdsvgpd44 0.000000 0.000000
## z131idupvn3yhf3mv23dwzhi4pqixvwuw 1.456885 1.456885
## z132cvvy1ob3ht2er23dundqdtertjmlg 0.000000 0.000000
## _2viQ_Qnc69MEEHHJxZ427KX8MlljJPnUC2YBbvbWwY 0.000000 0.000000
## _2viQ_Qnc6_RKHVetk9kLzx8ZC62_J7y73FWFSBTe8Q 0.000000 0.000000
Create the test and train datasets
We had already split the dataset by marking rows as “train” or “test.”
Then subset the main matrix (yt_m) into training (xs_train) and testing (xs_test) based on the IDs.
Finally, match the labels (class column) to the corresponding IDs, creating factor variables for ‘ham’ vs. ‘spam’.
This gives a standard X_train, y_train, X_test, y_test setup for modeling and evaluation.
id_train <- yt %>% filter(train == 'train') %>% pull(id) # grab id's if train
id_test <- yt %>% filter(train == 'test') %>% pull(id) # grab id's for test
xs_train <- yt_m[which(rownames(yt_m) %in% id_train),] # filter matrix for training
xs_test <- yt_m[which(rownames(yt_m) %in% id_test),] # filter matrix for testing
ys_train <- yt %>% filter(id %in% rownames(xs_train)) %>%
pull(class) %>% # filter class for training id (labels)
factor(levels = c(0, 1), labels = c('ham', 'spam'))
ys_test<- yt %>% filter(id %in% rownames(xs_test)) %>%
pull(class) %>% # filter class for testing id (labels)
factor(levels = c(0, 1), labels = c('ham', 'spam'))
NOTE!!! If you want to run this, do it later. xs_train (our matrix
for prediction) is very large. It will take ages to run as it has a lot
of variables (over 3700) - you can check with dim(xs_train)
You can just load the model that has been already fit.
library(pROC)
## Type 'citation("pROC")' for a citation.
##
## Attaching package: 'pROC'
## The following objects are masked from 'package:stats':
##
## cov, smooth, var
library(randomForest)
## randomForest 4.7-1.2
## Type rfNews() to see new features/changes/bug fixes.
##
## Attaching package: 'randomForest'
## The following object is masked from 'package:dplyr':
##
## combine
## The following object is masked from 'package:ggplot2':
##
## margin
# rf1 <- randomForest(x = xs_train, y = ys_train, do.trace = T,
# ntree = 90)
# save.image(file = 'tubespam_rf1.rda')
load('./tubespam_rf1.rda')
OMG, this model is horribly overfit. How do we know?
#train
pred <- tibble(spam_p = predict(rf1, newdata = xs_train, type = 'prob')[,'spam'],
spam = ys_train)
yt_roc_train <- roc(data = pred, response = spam, predictor = spam_p)
## Setting levels: control = ham, case = spam
## Setting direction: controls < cases
#test
pred <- tibble(spam_p = predict(rf1, newdata = xs_test, type = 'prob')[,'spam'],
spam = ys_test)
yt_roc_test <- roc(data = pred, response = spam, predictor = spam_p)
## Setting levels: control = ham, case = spam
## Setting direction: controls < cases
# using base plotting
plot(yt_roc_train, print.auc = T)
lines(yt_roc_test, lty = 2, col = 'blue', print.auc = T)
text(round(auc(yt_roc_test), 3), x = 0.5, y = 0.4, col = "blue", pos = 4)
Might be worth reducing the mtry (number of variables to use)
In randomForest package, mtry controls how
many features (predictor variables) are randomly sampled at each split
in a decision tree.
If your dataset has a very large number of features, lowering
mtry can sometimes improve performance by reducing
overfitting and promoting greater diversity across individual trees.
However, setting it too low may hamper the forest’s ability to find
strong predictors.
A feature importance plot helps you see which variables are driving the classification decisions. Importance can be measured via metrics like like the Mean Decrease in Gini (how much splitting on a variable improves node purity).
If you notice that only a few variables dominate the importance
chart, or your model takes very long to train, you might try lowering
mtry.
This can:
Prevent a handful of strong features from always being chosen.
Encourage your model to consider a broader range of features at each split, potentially improving generalisation.
varImpPlot(rf1)
Create generalized linguistic features. First I have to load up a linguistic dictionary called NRC Emotion Lexicon. It is available here: https://saifmohammad.com/WebPages/NRC-Emotion-Lexicon.htm And is free for use for research or education, with acknowledgement. Thank you Dr. Saif M. Mohammad at the National Research Council Canada.
I have to read it in as a text file, then convert it to a different format that we can use. You can open up the text file and check out what Dr Mohammad has done to classify words as emotions.
# Split each line into components and convert to a data frame
nrc <- readLines("NRC-Emotion-Lexicon-Wordlevel-v0.92.txt")
data <- do.call(rbind, strsplit(nrc, "\t"))
df <- as.data.frame(data, stringsAsFactors = FALSE)
colnames(df) <- c("Word", "Emotion", "Association")
# Convert Association column to numeric
df$Association <- as.numeric(df$Association)
# Filter out rows where Association is 0, as we're only interested in associations marked as 1
df <- df[df$Association == 1, ]
# Create an empty list to store emotions as keys and associated words as values
emotion_words_dict <- list()
# Populate the list
for (row in 1:nrow(df)) {
emotion <- df$Emotion[row]
word <- df$Word[row]
# Check if the emotion already exists as a key in the list
if (!is.null(emotion_words_dict[[emotion]])) {
# Append the word to the existing list of words for this emotion
emotion_words_dict[[emotion]] <- c(emotion_words_dict[[emotion]], word)
} else {
# Create a new entry in the list with this emotion as the key and the word as the value
emotion_words_dict[[emotion]] <- c(word)
}
}
emotion_dict <- dictionary(emotion_words_dict)
# Check the structure of the created dictionary
print(emotion_dict)
## Dictionary object with 10 key entries.
## - [trust]:
## - abacus, abbot, absolution, abundance, academic, accolade, accompaniment, accord, account, accountability, accountable, accountant, accounts, accredited, accurate, achieve, achievement, acrobat, adhering, administrative [ ... and 1,210 more ]
## - [fear]:
## - abandon, abandoned, abandonment, abduction, abhor, abhorrent, abominable, abomination, abortion, absence, abuse, abyss, accident, accidental, accursed, accused, accuser, accusing, acrobat, adder [ ... and 1,454 more ]
## - [negative]:
## - abandon, abandoned, abandonment, abduction, aberrant, aberration, abhor, abhorrent, abject, abnormal, abolish, abolition, abominable, abomination, abort, abortion, abortive, abrasion, abrogate, abscess [ ... and 3,296 more ]
## - [sadness]:
## - abandon, abandoned, abandonment, abduction, abortion, abortive, abscess, absence, absent, absentee, abuse, abysmal, abyss, accident, accursed, ache, aching, adder, adrift, adultery [ ... and 1,167 more ]
## - [anger]:
## - abandoned, abandonment, abhor, abhorrent, abolish, abomination, abuse, accursed, accusation, accused, accuser, accusing, actionable, adder, adversary, adverse, adversity, advocacy, affront, aftermath [ ... and 1,225 more ]
## - [surprise]:
## - abandonment, abduction, abrupt, accident, accidental, accidentally, accolade, advance, affront, aghast, alarm, alarming, alertness, alerts, allure, amaze, amazingly, ambush, angel, anomaly [ ... and 512 more ]
## [ reached max_nkey ... 4 more keys ]
I have now used it by applying it to our raw content.
dic_features <- yt %>%
with(liwcalike(corpus(content, docnames = id),
dictionary = emotion_dict,tolower = T)) %>%
rename(id = docname)
We want to find if there are URL’s in the comment. Google provided an answer:
https://stackoverflow.com/questions/3809401/what-is-a-good-regular-expression-to-match-a-url
# website_regex <- "(www\\.)?([a-zA-Z0-9]+(-?[a-zA-Z0-9])*\\.)+[\\w]{2,}(\\/\\S*)?"
website_regex <- "[-a-zA-Z0-9@:%._\\+~#=]{1,}\\.[a-zA-Z0-9()]{1,6}\\b([-a-zA-Z0-9()@:%_\\+.~#?&//=]*)"
yt_url <- yt %>%
ungroup() %>%
mutate(content = tolower(content)) %>%
mutate(url = grepl(website_regex, content)) %>% #transform to lowercase and use grepl pattern match
mutate(url = ifelse(url, 1, 0)) %>%
select(id, url)
Now use left_join to join our url findings to our dic_features (joining by id) re-join with original youtube data so can get original info
yt_ling_features <- left_join(dic_features, yt_url) %>%
left_join(select(yt, id, class, train, content)) %>%
mutate(class = as.factor(class))
## Joining with `by = join_by(id)`
## Joining with `by = join_by(id)`
glimpse(yt_ling_features)
## Rows: 1,710
## Columns: 32
## $ id <chr> "LZQPQhLyRh80UYxNuaDWhIGQYNQ96IuCg-AYWqNPjpU", "LZQPQhLyR…
## $ Segment <int> 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17…
## $ WPS <dbl> 13.00, 19.50, 8.00, 12.00, 4.50, 9.50, 4.00, 21.00, 10.00…
## $ WC <int> 13, 39, 8, 12, 9, 19, 4, 21, 10, 15, 4, 37, 7, 3, 15, 71,…
## $ Sixltr <dbl> 15.38, 10.26, 12.50, 16.67, 11.11, 10.53, 50.00, 0.00, 10…
## $ Dic <dbl> 0.00, 10.26, 0.00, 58.33, 22.22, 0.00, 25.00, 0.00, 0.00,…
## $ trust <dbl> 0.00, 0.00, 0.00, 8.33, 0.00, 0.00, 0.00, 0.00, 0.00, 0.0…
## $ fear <dbl> 0.00, 0.00, 0.00, 8.33, 11.11, 0.00, 0.00, 0.00, 0.00, 0.…
## $ negative <dbl> 0.00, 2.56, 0.00, 16.67, 0.00, 0.00, 0.00, 0.00, 0.00, 0.…
## $ sadness <dbl> 0.00, 2.56, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.0…
## $ anger <dbl> 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.0…
## $ surprise <dbl> 0.00, 2.56, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.0…
## $ positive <dbl> 0.00, 0.00, 0.00, 8.33, 0.00, 0.00, 0.00, 0.00, 0.00, 0.0…
## $ disgust <dbl> 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.0…
## $ joy <dbl> 0.00, 0.00, 0.00, 8.33, 0.00, 0.00, 0.00, 0.00, 0.00, 0.0…
## $ anticipation <dbl> 0.00, 2.56, 0.00, 8.33, 11.11, 0.00, 25.00, 0.00, 0.00, 0…
## $ AllPunc <dbl> 30.77, 23.08, 12.50, 8.33, 22.22, 26.32, 0.00, 14.29, 20.…
## $ Period <dbl> 0.00, 0.00, 12.50, 0.00, 11.11, 10.53, 0.00, 14.29, 0.00,…
## $ Comma <dbl> 7.69, 2.56, 0.00, 0.00, 0.00, 5.26, 0.00, 0.00, 0.00, 0.0…
## $ Colon <dbl> 7.69, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.0…
## $ SemiC <dbl> 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.0…
## $ QMark <dbl> 0.00, 0.00, 0.00, 0.00, 11.11, 0.00, 0.00, 0.00, 0.00, 0.…
## $ Exclam <dbl> 0.00, 17.95, 0.00, 0.00, 0.00, 10.53, 0.00, 0.00, 20.00, …
## $ Dash <dbl> 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.0…
## $ Quote <dbl> 0.00, 2.56, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.0…
## $ Apostro <dbl> 0.00, 2.56, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.0…
## $ Parenth <dbl> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, …
## $ OtherP <dbl> 15.38, 23.08, 12.50, 0.00, 22.22, 26.32, 0.00, 14.29, 20.…
## $ url <dbl> 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 1, 0, 0, 1, 1, …
## $ class <fct> 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, …
## $ train <chr> "train", "train", "train", "train", "train", "train", "tr…
## $ content <chr> "Huh, anyway check out this you[tube] channel: kobyoshi02…
filter into test and train and remove features we don’t want
yt_ling_train <- yt_ling_features %>%
filter(train == 'train') %>%
select(-id, -train, -content, -Segment) #remove variables we don't want
yt_ling_test <- yt_ling_features %>%
filter(train == 'test') %>%
select(-id, -train, -content, -Segment)
save(yt_ling_features, yt_ling_train, yt_ling_test, file = 'youtube_spam_train_test.rda')
rf2 <- randomForest(class ~ .,
data = yt_ling_train,
ntree = 150)
It’s still overfit, but its a lot better for the test set. Now, instead of using specific words to predict, we are using generalisable emotions to predict
#train
pred <- tibble(spam_p = predict(rf2, newdata = yt_ling_train, type = 'prob')[,'1'],
spam = yt_ling_train$class)
yt_roc_train <- roc(data = pred, response = spam, predictor = spam_p)
## Setting levels: control = 0, case = 1
## Setting direction: controls < cases
#test
pred <- tibble(spam_p = predict(rf2, newdata = yt_ling_test, type = 'prob')[,'1'],
spam = yt_ling_test$class)
yt_roc_test <- roc(data = pred, response = spam, predictor = spam_p)
## Setting levels: control = 0, case = 1
## Setting direction: controls < cases
# using base plotting
plot(yt_roc_train, print.auc = T)
lines(yt_roc_test, lty = 2, col = 'blue', print.auc = T)
text(round(auc(yt_roc_test), 3), x = 0.5, y = 0.4, col = "blue", pos = 4)
We don’t know the causal direction, so like your assignment, we will create some fake data.
varImpPlot(rf2)
First using fake data with 1000 values!
#set.seed(1223)
N <- 1000
g <- seq(min(yt_ling_train$Sixltr),
max(yt_ling_train$Sixltr),
length.out = N)
#d <- sample_n(yt_ling_train, 1)
d <- yt_ling_features[38,]
#quick check of a prediction (its a true positive)
predict(rf2, newdata = d)
## 38
## 1
## Levels: 0 1
dn <- d %>%
mutate(n = N) %>%
uncount(n) %>%
mutate(Sixltr = g)
dn$pred <- predict(rf2, newdata = dn, type = "prob")[,2]
#Lets play with the most important variable a bit before we look
predict(rf2, newdata =d, type="prob")
## 0 1
## 38 0.1 0.9
## attr(,"class")
## [1] "matrix" "array" "votes"
d_change <- d %>% mutate(Sixltr = 50)
predict(rf2, newdata =d_change, type="prob")
## 0 1
## 38 0.1 0.9
## attr(,"class")
## [1] "matrix" "array" "votes"
d_change2 <- d %>% mutate(Sixltr = 2)
predict(rf2, newdata =d_change2, type="prob")
## 0 1
## 38 0.3733333 0.6266667
## attr(,"class")
## [1] "matrix" "array" "votes"
ggplot(dn, aes(x = Sixltr, y = pred)) +
geom_line() +
coord_cartesian(xlim = c(0, 100)) +
xlab("Percent of words that are six letters or longer") +
ylab("P(spam)")
library(iml)
d <- sample_n(yt_ling_train, 200)
yt_pred <- Predictor$new(model = rf2, data = d,
y = d$class, type = "prob")
yt_effect <- FeatureEffect$new(yt_pred,center.at = 0.5,
grid.size = 100,
method = "pdp",
feature = "Sixltr")
yt_effect$plot() +
xlab("Percent of words that are six letters or longer") +
ylab("P(spam)")
—- We can stop here for today! —–
Prep the term document matrix for counts instead of TD-IDF We don’t need to clean, will use all of the words
library(topicmodels)
library(LDAvis)
library(gt)
yt_dtm <- yt_tokens %>%
count(id, word) %>%
cast_dtm(id, word, n)
Choose the number of topics and run the LDA.
k <- 10
seed <- 2015
yt_lda <- LDA(yt_dtm, k=k, method = 'Gibbs', control=list(seed=seed, burnin=1000, thin=100, iter=1000))
Top 10 terms from each topic.
terms(yt_lda, 10) %>%
as_tibble() %>%
gt
| Topic 1 | Topic 2 | Topic 3 | Topic 4 | Topic 5 | Topic 6 | Topic 7 | Topic 8 | Topic 9 | Topic 10 |
|---|---|---|---|---|---|---|---|---|---|
| br | guys | https | amp | check | video | views | 39 | subscribe | song |
| quot | free | im | lt | http | youtube | billion | music | channel | love |
| youtube | money | www.facebook.com | nice | mixtape | check | shakira | comment | watch | katy |
| share | visit | watching | gt | awesome | playlist | people | time | videos | perry |
| waka | day | ref | hear | href | channel | pray | world | follow | songs |
| fucking | cool | 3 | 48051 | it | song | thumbs | shit | girl | |
| click | website | 2015 | style | image2you.ru | party | wow | chance | hate | beautiful |
| rapper | paid | guy | gangnam | vote | megan | subscribers | money | plz | omg |
| birthday | class | http | support | hey | mother | covers | funny | remember | |
| miley | online | dance | songs | rand | rock | d | hey | hey | roar |
LDA visualization tool.
lda_json <- createJSON(phi = posterior(yt_lda)$terms,
theta = posterior(yt_lda)$topics,
vocab = posterior(yt_lda)$terms %>% colnames,
doc.length = rowSums(as.matrix(yt_dtm)),
term.frequency = colSums(as.matrix(yt_dtm)))
serVis(lda_json)
## Loading required namespace: servr
Predicting with topics.
You end up with a single dataset (yt_topics) that contains:
All original columns from yt (id, class, etc.) Topic proportion columns (Topic_1, Topic_2, …, Topic_k) for each document. This allows you to use the topics as features for further analysis, such as classification, clustering, or just exploring how topics vary with the class label.
posterior(yt_lda)$topics[1:5, 1:5]
## 1 2 3
## LZQPQhLyRh80UYxNuaDWhIGQYNQ96IuCg-AYWqNPjpU 0.09090909 0.09090909 0.10909091
## LZQPQhLyRh9-wNRtlZDM90f1k0BrdVdJyN_YsaSwfxc 0.08771930 0.14035088 0.10526316
## LZQPQhLyRh9EXArr4ZnVcDonSbvSMHKYOT24e_qR6fE 0.09615385 0.09615385 0.09615385
## LZQPQhLyRh9MSZYnf8djyk0gEF9BHDPYrrK-qCczIY8 0.11538462 0.09615385 0.09615385
## LZQPQhLyRh9U7Lv_DKpJ7lawpBCotxfgHzBy93Tk028 0.09615385 0.09615385 0.09615385
## 4 5
## LZQPQhLyRh80UYxNuaDWhIGQYNQ96IuCg-AYWqNPjpU 0.10909091 0.09090909
## LZQPQhLyRh9-wNRtlZDM90f1k0BrdVdJyN_YsaSwfxc 0.08771930 0.08771930
## LZQPQhLyRh9EXArr4ZnVcDonSbvSMHKYOT24e_qR6fE 0.09615385 0.11538462
## LZQPQhLyRh9MSZYnf8djyk0gEF9BHDPYrrK-qCczIY8 0.09615385 0.09615385
## LZQPQhLyRh9U7Lv_DKpJ7lawpBCotxfgHzBy93Tk028 0.09615385 0.09615385
yt_topics <- posterior(yt_lda)$topics
colnames(yt_topics) <- paste0("Topic_", colnames(yt_topics))
yt_topics <- as_tibble(yt_topics) %>%
mutate(id = rownames(yt_topics))
yt_topics <- left_join(yt, yt_topics) %>%
mutate(class = as_factor(class)) %>%
na.omit()
## Joining with `by = join_by(id)`
Comments are weighted to every topic - we can use those to make predictions. Create our test/train sets
yt_topic_train <- yt_topics %>%
filter(train == "train") %>%
select(class, Topic_1:Topic_10)
yt_topic_test <- yt_topics %>%
filter(train == "test") %>%
select(class, Topic_1:Topic_10)
rf3 <- randomForest(class ~ ., data = yt_topic_train,
ntree = 300)
Not as good as the linguistic features, but better than raw words, but you could use both!
#train
pred <- tibble(spam_p = predict(rf3, newdata = yt_topic_train, type = 'prob')[,'1'],
spam = yt_topic_train$class)
yt_roc_train <- roc(data = pred, response = spam, predictor = spam_p)
## Setting levels: control = 0, case = 1
## Setting direction: controls < cases
#test
pred <- tibble(spam_p = predict(rf3, newdata = yt_topic_test, type = 'prob')[,'1'],
spam = yt_topic_test$class)
yt_roc_test <- roc(data = pred, response = spam, predictor = spam_p)
## Setting levels: control = 0, case = 1
## Setting direction: controls < cases
# using base plotting
plot(yt_roc_train, print.auc = T)
lines(yt_roc_test, lty = 2, col = 'blue', print.auc = T)
text(round(auc(yt_roc_test), 3), x = 0.5, y = 0.4, col = "blue", pos = 4)
Check most important variables
varImpPlot(rf3)
What does Topic 10 do?
d <- sample_n(yt_topic_train, 200)
yt_pred <- Predictor$new(model = rf3, data=d, y=d$class, type="prob")
yt_effect <- FeatureEffect$new(yt_pred, center.at = 0.5,
grid.size = 100,
method = "pdp+ice",
feature = "Topic_10")
yt_effect$plot() +
#coord_cartesian(xlim = c(0,100))+
xlab("Topic 10")+
ylab("P(spam)")+
scale_x_log10()