1. Executive Summary

This milestone report forms an integral part of the Johns Hopkins Data Science Capstone Project. The primary objective of this project is to construct a predictive text application capable of suggesting the most probable next word given a sequence of preceding words.

This report covers the initial phase of the data science lifecycle: 1. Loading and inspecting the raw HC Corpora text dataset (Blogs, News, and Twitter feeds). 2. Generating descriptive summary statistics (file sizes, line counts, word counts). 3. Executing clean text preprocessing and tokenization using quanteda and tidytext. 4. Performing exploratory data analysis (EDA) to evaluate Unigram, Bigram, and Trigram frequency distributions. 5. Outlining the architectural roadmap for building an N-gram language model with Stupid Back-off smoothing and deploying it via an interactive R Shiny application.


2. Environment Setup & Data Acquisition

To maintain execution speed and memory safety during text tokenization, we utilize high-performance R packages including tidyverse, quanteda, quanteda.textstats, and stringi.

# Core text mining and data manipulation libraries
library(tidyverse)
## Warning: package 'tidyverse' was built under R version 4.5.3
## Warning: package 'ggplot2' was built under R version 4.5.3
## Warning: package 'dplyr' was built under R version 4.5.3
## ── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ──
## ✔ dplyr     1.2.1     ✔ readr     2.1.6
## ✔ forcats   1.0.1     ✔ stringr   1.6.0
## ✔ ggplot2   4.0.3     ✔ tibble    3.3.0
## ✔ lubridate 1.9.4     ✔ tidyr     1.3.1
## ✔ purrr     1.2.0     
## ── 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)
## Warning: package 'tidytext' was built under R version 4.5.3
library(quanteda)
## Warning: package 'quanteda' was built under R version 4.5.3
## Package version: 4.5.0
## Unicode version: 15.1
## ICU version: 74.1
## Parallel computing: 2 of 2 threads used.
## See https://quanteda.io for tutorials and examples.
library(quanteda.textstats)
## Warning: package 'quanteda.textstats' was built under R version 4.5.3
library(stringi)
library(knitr)

The raw text files (en_US.blogs.txt, en_US.news.txt, and en_US.twitter.txt) are ingested using UTF-8 encoding.

# Define file path and URL
file_url <- "https://d396qusza40orc.cloudfront.net/dsscapstone/dataset/Coursera-SwiftKey.zip"
zip_file <- "Coursera-SwiftKey.zip"

# Download and unzip dataset if it doesn't exist
if (!file.exists("en_US.blogs.txt")) {
  if (!file.exists(zip_file)) {
    download.file(file_url, destfile = zip_file, method = "curl")
  }
  unzip(zip_file)
  
# The zip extracts files inside 'final/en_US/' subfolder — copy them to working directory
  file.copy("final/en_US/en_US.blogs.txt", "en_US.blogs.txt")
  file.copy("final/en_US/en_US.news.txt", "en_US.news.txt")
  file.copy("final/en_US/en_US.twitter.txt", "en_US.twitter.txt")
}

# Read raw text files
blogs_raw   <- readLines("en_US.blogs.txt", encoding = "UTF-8", skipNul = TRUE)
news_raw    <- readLines("en_US.news.txt", encoding = "UTF-8", skipNul = TRUE)
twitter_raw <- readLines("en_US.twitter.txt", encoding = "UTF-8", skipNul = TRUE)

3. Exploratory Summary Statistics

Before filtering or sampling, we assess the raw data size, record counts, total word counts, and average line lengths.

# Calculate file properties and metrics
get_file_size_mb <- function(file_path) {
  if (file.exists(file_path)) {
    return(round(file.info(file_path)$size / (1024^2), 2))
  } else {
    return(NA)
  }
}

summary_df <- tibble(
  Dataset = c("Blogs", "News", "Twitter"),
  `File Size (MB)` = c(
    get_file_size_mb("en_US.blogs.txt"),
    get_file_size_mb("en_US.news.txt"),
    get_file_size_mb("en_US.twitter.txt")
  ),
  `Line Count` = c(length(blogs_raw), length(news_raw), length(twitter_raw)),
  `Word Count` = c(
    sum(stri_count_words(blogs_raw)),
    sum(stri_count_words(news_raw)),
    sum(stri_count_words(twitter_raw))
  )
) %>%
  mutate(`Mean Words per Line` = round(`Word Count` / `Line Count`, 2))

# Render clean markdown table
knitr::kable(summary_df, caption = "Table 1: Summary Statistics of HC Corpora Datasets")
Table 1: Summary Statistics of HC Corpora Datasets
Dataset File Size (MB) Line Count Word Count Mean Words per Line
Blogs 200.42 899288 37546806 41.75
News 196.28 1010206 34761151 34.41
Twitter 159.36 2360148 30096690 12.75

4. Sampling & Text Preprocessing

Due to the substantial computational cost of processing full corpora in memory, we extract a 1% binomial random sample from each source to form a combined working corpus.

set.seed(42)
sample_rate <- 0.01

blogs_sample   <- sample(blogs_raw, length(blogs_raw) * sample_rate)
news_sample    <- sample(news_raw, length(news_raw) * sample_rate)
twitter_sample <- sample(twitter_raw, length(twitter_raw) * sample_rate)

combined_sample <- c(blogs_sample, news_sample, twitter_sample)

# Create Quanteda Corpus object
corp <- corpus(combined_sample)

# Tokenize with cleaning transformations
toks <- tokens(
  corp,
  remove_punct = TRUE,
  remove_symbols = TRUE,
  remove_numbers = TRUE,
  remove_url = TRUE
) %>%
  tokens_tolower()

5. Exploratory Data Analysis: N-Gram Frequencies

We construct Document-Feature Matrices (DFM) to evaluate the top frequency distributions for Unigrams (single words), Bigrams (2-word pairs), and Trigrams (3-word sequences).

5.1 Top Unigrams

unigram_dfm <- tokens_ngrams(toks, n = 1) %>% dfm()
top_unigrams <- textstat_frequency(unigram_dfm, n = 15)

ggplot(top_unigrams, aes(x = reorder(feature, frequency), y = frequency)) +
  geom_col(fill = "#2c3e50") +
  coord_flip() +
  labs(
    title = "Top 15 Most Frequent Unigrams",
    x = "Unigram Token",
    y = "Frequency Count"
  ) +
  theme_minimal()

5.2 Top Bigrams

bigram_dfm <- tokens_ngrams(toks, n = 2) %>% dfm()
top_bigrams <- textstat_frequency(bigram_dfm, n = 15)

ggplot(top_bigrams, aes(x = reorder(feature, frequency), y = frequency)) +
  geom_col(fill = "#18bc9c") +
  coord_flip() +
  labs(
    title = "Top 15 Most Frequent Bigrams",
    x = "Bigram Sequence",
    y = "Frequency Count"
  ) +
  theme_minimal()

5.3 Top Trigrams

trigram_dfm <- tokens_ngrams(toks, n = 3) %>% dfm()
top_trigrams <- textstat_frequency(trigram_dfm, n = 15)

ggplot(top_trigrams, aes(x = reorder(feature, frequency), y = frequency)) +
  geom_col(fill = "#e74c3c") +
  coord_flip() +
  labs(
    title = "Top 15 Most Frequent Trigrams",
    x = "Trigram Sequence",
    y = "Frequency Count"
  ) +
  theme_minimal()


6. Next Steps & Predictive Modeling Strategy

With data exploration completed, the next phase focuses on building and optimizing the prediction model for real-time web deployment:

  1. N-Gram Lookup Tables (2-gram to 4-gram):
    • Generate contiguous N-gram frequency tables stored as optimized data.table structures.
    • Split N-grams into prefix input contexts and target output prediction words.
  2. Stupid Back-Off Algorithm:
    • Implement a high-performance Stupid Back-off algorithm ($ lpha = 0.4$).
    • If a 4-gram match is not found in the lookup table, the model drops the leading word and evaluates 3-gram frequencies, decaying score values exponentially.
  3. Model Compression & Pruning:
    • Prune singletons (N-grams occurring only once) to compress total memory size to under 30 MB.
    • Save compressed structures into pre-computed binary .rds files.
  4. Shiny Application & UI:
    • Build a lightweight, responsive R Shiny application featuring reactive text inputs and instantaneous next-word suggestions.