Welcome to my Milestone Report for the Coursera Data Science Capstone Project, developed in partnership with SwiftKey.
The overall mission of this project is to build an intelligent, responsive text prediction system—much like the smart keyboard technology millions of us rely on daily on our smartphones. When a user begins typing a sentence, the application should accurately anticipate and recommend the most likely next word.
Before diving into complex machine learning architectures, a foundational exploratory analysis is essential. In this report, I document: * How I obtained, inspected, and verified the raw English corpora (blogs, news, and tweets). * The sampling and data cleaning strategy developed to handle over 100 million words efficiently. * Key patterns uncovered through unigram, bigram, and trigram linguistic distributions. * A clear, non-technical roadmap detailing how these findings will translate into our predictive model and interactive Shiny web application.
The training data consists of three distinct text sources written in
US English: 1. Blogs (en_US.blogs.txt):
Personal and opinion pieces, generally featuring rich vocabulary,
expressive descriptions, and longer paragraph structures. 2.
News (en_US.news.txt): Professional
journalism characterized by formal grammar, edited sentence structures,
and factual reporting. 3. Twitter
(en_US.twitter.txt): Casual micro-messages rich in
colloquialisms, abbreviations, hashtags, and a strict historic limit of
140 characters per tweet.
To understand the scope and scale of what we are dealing with, here is an initial summary of the raw files:
# Locate data folder across workspace or downloads
data_dir <- if (dir.exists("data/raw/Coursera-SwiftKey/final/en_US")) {
"data/raw/Coursera-SwiftKey/final/en_US"
} else if (dir.exists("../data/raw/Coursera-SwiftKey/final/en_US")) {
"../data/raw/Coursera-SwiftKey/final/en_US"
} else {
"C:/Users/miaae/Downloads/Coursera-SwiftKey/final/en_US"
}
files <- c(
"Blogs" = file.path(data_dir, "en_US.blogs.txt"),
"News" = file.path(data_dir, "en_US.news.txt"),
"Twitter" = file.path(data_dir, "en_US.twitter.txt")
)
# Helper function to read text safely in binary mode
# This prevents premature EOF halts on en_US.news.txt due to control characters
read_lines_safe <- function(path) {
con <- file(path, open = "rb")
lines <- readLines(con, encoding = "UTF-8", skipNul = TRUE, warn = FALSE)
close(con)
return(lines)
}
# High-level corpus metrics table
corpus_overview <- data.frame(
Source = c("Blogs", "News", "Twitter"),
FileName = basename(files),
Size_MB = c(200.42, 196.28, 159.36),
LineCount = c(899288, 1010242, 2360148),
WordCount = c(37546239, 34762395, 30093413),
MaxLineLength = c(40833, 11384, 140)
)
kable(
corpus_overview,
col.names = c("Source", "File Name", "Size (MB)", "Lines", "Total Words", "Longest Line (chars)"),
format.args = list(big.mark = ","),
caption = "Table 1: High-Level Summary of Raw en_US Corpora"
)| Source | File Name | Size (MB) | Lines | Total Words | Longest Line (chars) |
|---|---|---|---|---|---|
| Blogs | en_US.blogs.txt | 200.42 | 899,288 | 37,546,239 | 40,833 |
| News | en_US.news.txt | 196.28 | 1,010,242 | 34,762,395 | 11,384 |
| en_US.twitter.txt | 159.36 | 2,360,148 | 30,093,413 | 140 |
Working with 102 million words directly on every experimental iteration quickly consumes gigabytes of memory and leads to sluggish training cycles. Furthermore, our target production environment (ShinyApps.io) provides constrained memory (~1 GB RAM).
In statistical natural language processing, a clean, representative
random sample reflects the underlying vocabulary and n-gram frequencies
of the broader language with high statistical fidelity. For our
exploratory phase, I extracted a 1% pseudo-random
sample from each file. Setting a fixed random seed
(set.seed(42)) ensures complete reproducibility whenever
this report is re-run.
set.seed(42)
sample_fraction <- 0.01
raw_blogs <- read_lines_safe(files["Blogs"])
raw_news <- read_lines_safe(files["News"])
raw_twitter <- read_lines_safe(files["Twitter"])
sample_blogs <- sample(raw_blogs, length(raw_blogs) * sample_fraction)
sample_news <- sample(raw_news, length(raw_news) * sample_fraction)
sample_twitter <- sample(raw_twitter, length(raw_twitter) * sample_fraction)
# Consolidate into a unified tibble
sample_corpus <- tibble(
source = c(
rep("Blogs", length(sample_blogs)),
rep("News", length(sample_news)),
rep("Twitter", length(sample_twitter))
),
text = c(sample_blogs, sample_news, sample_twitter)
)
# Release raw lines from memory
rm(raw_blogs, raw_news, raw_twitter, sample_blogs, sample_news, sample_twitter)
invisible(gc())
cat("Successfully sampled", format(nrow(sample_corpus), big.mark = ","), "lines for exploratory modeling.")## Successfully sampled 42,695 lines for exploratory modeling.
Raw internet text contains a lot of digital noise that can distort a predictive model. I established the following preprocessing pipeline:
http://...), email addresses, and Twitter
@mentions that do not represent standard language
constructs.An n-gram is simply a contiguous sequence of n words collected from text. Studying n-grams reveals the natural cadence and common phrasings people use.
Unigrams reflect individual word frequencies across our unified sample.
unigrams <- sample_corpus %>%
unnest_tokens(word, text) %>%
filter(!grepl("^[0-9]+$", word)) %>%
count(word, sort = TRUE)
top_unigrams <- head(unigrams, 15)
ggplot(top_unigrams, aes(x = reorder(word, n), y = n)) +
geom_bar(stat = "identity", fill = "#34495e", width = 0.7) +
coord_flip() +
labs(
title = "Top 15 Most Common Words (Unigrams)",
subtitle = "Aggregated across sampled Blogs, News, and Twitter data",
x = "Word",
y = "Frequency Count"
) +
scale_y_continuous(labels = scales::comma) +
theme_minimal(base_size = 12) +
theme(
plot.title = element_text(face = "bold", color = "#2c3e50"),
panel.grid.minor = element_blank()
)The most frequent unigrams are foundational English building blocks: “the”, “to”, “and”, “a”, and “of”. These words form the grammatical backbone connecting our thoughts.
Looking at word pairs begins to uncover contextual associations.
bigrams <- sample_corpus %>%
unnest_tokens(bigram, text, token = "ngrams", n = 2) %>%
filter(!is.na(bigram)) %>%
count(bigram, sort = TRUE)
top_bigrams <- head(bigrams, 15)
ggplot(top_bigrams, aes(x = reorder(bigram, n), y = n)) +
geom_bar(stat = "identity", fill = "#16a085", width = 0.7) +
coord_flip() +
labs(
title = "Top 15 Most Common Two-Word Phrases (Bigrams)",
subtitle = "Two-word sequences observed across the sample",
x = "Bigram Phrase",
y = "Frequency Count"
) +
scale_y_continuous(labels = scales::comma) +
theme_minimal(base_size = 12) +
theme(
plot.title = element_text(face = "bold", color = "#2c3e50"),
panel.grid.minor = element_blank()
)The bigram chart highlights universal prepositional constructs (“of the”, “in the”, “to the”, “on the”, “for the”) as well as essential infinitive verb pairings like “to be”.
Trigrams capture clear idiomatic expressions and conversational structures.
trigrams <- sample_corpus %>%
unnest_tokens(trigram, text, token = "ngrams", n = 3) %>%
filter(!is.na(trigram)) %>%
count(trigram, sort = TRUE)
top_trigrams <- head(trigrams, 15)
ggplot(top_trigrams, aes(x = reorder(trigram, n), y = n)) +
geom_bar(stat = "identity", fill = "#e67e22", width = 0.7) +
coord_flip() +
labs(
title = "Top 15 Most Common Three-Word Phrases (Trigrams)",
subtitle = "Three-word sequences providing rich contextual clues",
x = "Trigram Phrase",
y = "Frequency Count"
) +
scale_y_continuous(labels = scales::comma) +
theme_minimal(base_size = 12) +
theme(
plot.title = element_text(face = "bold", color = "#2c3e50"),
panel.grid.minor = element_blank()
)Trigrams showcase intuitive, real-world conversational phrases like “one of the”, “a lot of”, “as well as”, and courteous social acknowledgments like “thanks for the”. These patterns will serve as direct lookup keys when users type preceding words.
A natural concern when designing a mobile-ready algorithm is: How many unique words do we actually need to store to achieve reliable coverage?
By calculating cumulative frequency, we can measure how many distinct words are needed to cover 50% and 90% of all written instances:
unigrams <- unigrams %>%
mutate(
cumulative_count = cumsum(n),
coverage_pct = cumulative_count / sum(n)
)
words_for_50 <- which(unigrams$coverage_pct >= 0.50)[1]
words_for_90 <- which(unigrams$coverage_pct >= 0.90)[1]
total_unique_words <- nrow(unigrams)
coverage_summary <- data.frame(
TargetCoverage = c("50% of all text", "90% of all text", "100% (Entire Vocabulary)"),
WordsRequired = c(words_for_50, words_for_90, total_unique_words),
PctOfLexicon = round(c(words_for_50, words_for_90, total_unique_words) / total_unique_words * 100, 2)
)
kable(
coverage_summary,
col.names = c("Coverage Goal", "Unique Words Required", "% of Total Vocabulary"),
format.args = list(big.mark = ","),
caption = "Table 2: Lexical Coverage and Dictionary Truncation Analysis"
)| Coverage Goal | Unique Words Required | % of Total Vocabulary |
|---|---|---|
| 50% of all text | 148 | 0.27 |
| 90% of all text | 7,440 | 13.67 |
| 100% (Entire Vocabulary) | 54,445 | 100.00 |
This finding demonstrates Zipf’s Law in action: a remarkably small group of high-frequency words covers the majority of our everyday language. * By retaining words that cover ~90% of language and safely pruning one-off words and typos, we can shrink model memory consumption by over 80% while preserving top-tier predictive accuracy.
With the exploratory phase complete and the data well understood, here is the concrete execution plan for building the predictive engine and deploying the Shiny app:
data.table or serialized binary hash maps) to ensure
predictions resolve in under 25 milliseconds.This exploratory data analysis confirms that our text data is well structured, clean, and representative. The linguistic patterns discovered in our unigram, bigram, and trigram analyses provide a robust, evidence-based foundation for our backoff predictive algorithm.
Development is on track, and the next phase will focus on model training, vocabulary pruning, and delivering a clean, polished Shiny web application.