1 Introduction & Project Motivation

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.


2 Data Ingestion & File Summaries

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"
)
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
Twitter en_US.twitter.txt 159.36 2,360,148 30,093,413 140

2.0.1 What Stands Out:

  • Sheer Scale: Across the three sources, we have roughly 580 Megabytes of text spanning 4.26 million lines and over 102 million words.
  • Length Diversity: As expected, tweets are bounded at 140 characters, while blogs contain expansive streams of thought—reaching up to 40,833 characters in a single entry.

3 Sampling & Data Preparation

3.1 Why Sampling Makes Sense

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.

3.2 Cleaning Pipeline & Design Choices

Raw internet text contains a lot of digital noise that can distort a predictive model. I established the following preprocessing pipeline:

  1. Case Normalization: Converted all characters to lowercase so that words like “The”, “the”, and “THE” map to the same token.
  2. URL & Handle Stripping: Removed web links (http://...), email addresses, and Twitter @mentions that do not represent standard language constructs.
  3. Numbers & Punctuation: Removed stand-alone digits, symbols, and formatting marks, while keeping intra-word apostrophes for common contractions (e.g., “don’t”, “I’m”).
  4. Why Stopwords are Retained: In topic modeling or document classification, common stopwords (“the”, “and”, “in”, “to”) are usually stripped out because they don’t help differentiate topics. However, for next-word prediction, stopwords are critical. People type these functional connective words constantly, and omitting them would produce unnatural, broken suggestions.
  5. Profanity Filtering: In our eventual consumer-facing app, we will cross-reference candidate suggestions against a standard profanity lexicon so that offensive terms are filtered out of the recommendation choices.

4 Exploratory Findings & N-Gram Distributions

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.

4.1 1. Unigrams (Single Words)

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.

4.2 2. Bigrams (Two-Word Combinations)

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”.

4.3 3. Trigrams (Three-Word Phrases)

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.


5 Vocabulary Coverage: Unlocking Efficiency

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"
)
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

5.0.1 Strategic Takeaway:

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.


6 Strategy for Prediction Algorithm & Shiny App

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:

6.1 1. The Prediction Engine (Stupid Backoff / Katz Model)

  • Tiered Lookups: The engine will evaluate the user’s typed history hierarchically:
    1. If 3 or more words are provided, look for matching 4-grams to suggest word #4.
    2. If no 4-gram match exists, smoothly back off to the matching trigram (last 2 words).
    3. If still no match, back off to bigrams (last word).
    4. If the word is entirely unfamiliar, default to the top unigrams (“the”, “to”, etc.).
  • Speed Optimization: The n-gram frequencies will be pre-calculated and stored in ultra-fast indexed lookup tables (data.table or serialized binary hash maps) to ensure predictions resolve in under 25 milliseconds.

6.2 2. Interactive Shiny Application Design

  • Clean, Focused Interface: A minimalist text input field where the user types naturally.
  • Instant Word Suggestions: Three interactive recommendation buttons will appear dynamically directly above the cursor—just like a modern smartphone keyboard. Clicking any button will immediately insert the word and update the next suggestions.
  • Transparency & Feedback: A collapsible diagnostics panel will indicate which n-gram tier produced the recommendation (e.g., “Trigram match with 42% confidence”), giving both casual users and technical evaluators full visibility into the model’s inner workings.
  • Production Deployment: The finalized app will be deployed live to shinyapps.io, ensuring broad public accessibility with zero local installation required.

7 Summary

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.