The goal of this capstone is a Shiny app that predicts the next word as a user types, much like a phone keyboard. This report shows that the data has been downloaded and loaded, summarizes the three text files, and outlines the plan for the prediction algorithm.
Main findings so far
The data comes from the Coursera/SwiftKey corpus. Only the English
(en_US) files are used.
dir.create("data", showWarnings = FALSE)
options(timeout = 7200)
zip_path <- "data/Coursera-SwiftKey.zip"
url <- "https://d396qusza40orc.cloudfront.net/dsscapstone/dataset/Coursera-SwiftKey.zip"
if (!file.exists("data/final/en_US/en_US.blogs.txt")) {
if (!file.exists(zip_path)) download.file(url, zip_path, mode = "wb", method = "libcurl")
unzip(zip_path, exdir = "data")
}
read_text <- function(f) {
con <- file(f, "rb")
on.exit(close(con))
readLines(con, encoding = "UTF-8", skipNul = TRUE, warn = FALSE)
}
blogs <- read_text("data/final/en_US/en_US.blogs.txt")
news <- read_text("data/final/en_US/en_US.news.txt")
twitter <- read_text("data/final/en_US/en_US.twitter.txt")
summarize_source <- function(x, name, path) {
words <- stri_count_words(x)
data.frame(
Source = name,
`File size (MB)` = round(file.size(path) / 1024^2, 1),
Lines = length(x),
`Total words` = sum(words, na.rm = TRUE),
`Mean words per line` = round(mean(words, na.rm = TRUE), 1),
`Longest line (characters)` = max(nchar(x)),
check.names = FALSE
)
}
summary_tbl <- bind_rows(
summarize_source(blogs, "Blogs", "data/final/en_US/en_US.blogs.txt"),
summarize_source(news, "News", "data/final/en_US/en_US.news.txt"),
summarize_source(twitter, "Twitter", "data/final/en_US/en_US.twitter.txt")
)
kable(summary_tbl, format.args = list(big.mark = ","),
caption = "Line and word counts for each file")
| Source | File size (MB) | Lines | Total words | Mean words per line | Longest line (characters) |
|---|---|---|---|---|---|
| Blogs | 200.4 | 899,288 | 37,546,806 | 41.8 | 40,833 |
| News | 196.3 | 1,010,242 | 34,762,658 | 34.4 | 11,384 |
| 159.4 | 2,360,148 | 30,096,690 | 12.8 | 140 |
Because the files are big, the rest of the analysis uses a random 2% sample of each file. This is fast and still representative.
set.seed(123)
take <- function(x, p = 0.02) x[sample(length(x), round(length(x) * p))]
samp <- bind_rows(
data.frame(source = "Blogs", text = take(blogs), stringsAsFactors = FALSE),
data.frame(source = "News", text = take(news), stringsAsFactors = FALSE),
data.frame(source = "Twitter", text = take(twitter), stringsAsFactors = FALSE)
)
# Basic cleaning: lower case, drop URLs, keep letters and apostrophes only
samp$text <- tolower(samp$text)
samp$text <- gsub("http\\S+|www\\.\\S+", " ", samp$text)
samp$text <- gsub("[^a-z' ]", " ", samp$text)
samp$text <- gsub("\\s+", " ", trimws(samp$text))
samp <- samp[nchar(samp$text) > 0, ]
samp$n_words <- stri_count_words(samp$text)
ggplot(samp, aes(n_words, fill = source)) +
geom_histogram(binwidth = 2, show.legend = FALSE) +
coord_cartesian(xlim = c(0, 120)) +
facet_wrap(~source, scales = "free_y") +
labs(title = "Words per line (2% sample)", x = "Words in a line", y = "Number of lines")
Takeaway: Tweets are short (capped by Twitter’s length limit), while blog lines can be very long. The app should work well on short, casual text as well as longer prose.
unigrams <- samp %>%
unnest_tokens(word, text, token = "words") %>%
count(word, sort = TRUE)
unigrams %>% slice_head(n = 15) %>%
ggplot(aes(reorder(word, n), n)) +
geom_col(fill = "steelblue") + coord_flip() +
labs(title = "15 most frequent words", x = NULL, y = "Count")
unigrams <- unigrams %>% mutate(cum_share = cumsum(n) / sum(n), rank = row_number())
w50 <- min(which(unigrams$cum_share >= 0.5))
w90 <- min(which(unigrams$cum_share >= 0.9))
ggplot(unigrams, aes(rank, cum_share)) +
geom_line(color = "darkred", linewidth = 1) +
scale_x_log10(labels = scales::comma) +
scale_y_continuous(labels = scales::percent) +
geom_hline(yintercept = c(0.5, 0.9), linetype = "dashed") +
labs(title = "Share of all words covered by the most frequent words",
x = "Number of unique words (log scale)", y = "Coverage of all word occurrences")
Out of 73,072 distinct words in the sample, only 143 words cover 50% of everything written and 7,053 words cover 90%. The rare words (the long tail) can be trimmed to keep the app small and fast.
top_ngrams <- function(n) {
samp %>%
unnest_tokens(ngram, text, token = "ngrams", n = n) %>%
filter(!is.na(ngram)) %>%
count(ngram, sort = TRUE)
}
bigrams <- top_ngrams(2)
trigrams <- top_ngrams(3)
plot_top <- function(df, ttl) {
df %>% slice_head(n = 15) %>%
ggplot(aes(reorder(ngram, n), n)) +
geom_col(fill = "seagreen") + coord_flip() +
labs(title = ttl, x = NULL, y = "Count")
}
plot_top(bigrams, "15 most frequent two-word phrases")
plot_top(trigrams, "15 most frequent three-word phrases")
Takeaway: Everyday phrases such as “of the” and “thanks for the” repeat constantly. If a user has typed “thanks for”, the model can learn that “the” is a very likely next word.
Algorithm
Shiny app
Feedback requested: Is a 2-4 word context the right trade-off between accuracy and app size, and are there other features worth adding?