pythonintermediate45 minPython 3

Streaming N-Gram Frequency Text Analyzer

Build a streaming text analyzer in Python that computes n-gram word frequencies, handles empty inputs safely, and filters stop words.

The incident

Company
EchoNLP Analytics
Engineer
Karan Singhania, Data Processing Engineer
Trigger
Customer sentiment feedback jobs crashed on empty reviews, and un-sanitized punctuation skewed frequency rankings.
Stakes
Marketing team relied on customer feedback insights from 40,000 weekly product reviews; pipeline crashes delayed weekly reporting by 3 business days.

At EchoNLP Analytics, the customer intelligence team processed over forty thousand customer product reviews each week across e-commerce partner platforms and retail merchant networks. The automated data pipeline analyzed raw text feedback to identify emerging product complaints, feature requests, and positive sentiment themes for merchant brand managers. However, every Monday morning during batch report generation, the text parsing pipeline crashed intermittently with unhandled exceptions, stalling automated report generation across marketing channels, delaying strategic executive readouts, and consuming developer on-call hours across platform maintenance rotations.

The pipeline crashes were traced to edge-case reviews: customers frequently submitted blank feedback forms, reviews containing only punctuation marks like question marks or ellipses, or emoji-only comments without words. When the text processing script called collections.Counter.most_common(1)[0] on an empty word counter, Python raised an unhandled IndexError, halting the entire batch job and preventing forty thousand reviews from being indexed for downstream analytics dashboards and merchant performance reviews across client brand portals and business intelligence suites.

The operational fallout caused severe business friction between product and marketing leadership. Weekly marketing intelligence readouts were delayed by up to three business days while on-call data engineers manually sorted through corrupted text lines to locate the offending records. Brand managers complained that critical customer feedback regarding defective product batches went unnoticed until executive dashboards were updated days later, resulting in missed customer resolution windows, lost sales, and unaddressed product quality complaints across partner retail channels.

Karan Singhania, a data processing engineer on the language engineering team, was assigned to rebuild the text processing pipeline from scratch. The new text analyzer needed to normalize raw strings defensively, strip non-alphanumeric punctuation cleanly, extract both unigram (single-word) and bigram (two-word) keyphrases using sliding window iterators, and filter out common grammatical stop-words that distorted frequency rankings across feedback cohorts and product categories.

Crucially, the analyzer needed to handle empty strings and degenerate inputs without crashing. Karan implemented defensive guards around frequency counting, verifying that Counter.most_common(n) returned non-empty lists before indexing. By combining regular expression tokenization with collections.Counter, the new utility returned the same frequency table every time for the same reviews, and returned an empty result for a blank review instead of raising an error that stopped the whole batch.

When deployed to the production analytics cluster, the streaming n-gram analyzer processed the entire weekly backlog without crashing, accurately highlighting critical customer complaint bigrams like battery drain and poor fit. The tokenization rules and sentiment keyphrase reports are reviewed weekly by the Marketing Insights Director and Lead Data Architect to ensure robust analytical tracking and high-quality merchant intelligence reporting for enterprise retail clients across global markets and multi-category e-commerce portals.

What you will learn

  • Normalize and tokenize raw unstructured text using regular expressions
  • Construct multi-word n-gram sliding windows in Python
  • Guard collections.Counter against IndexError when indexing most_common(n)
  • Filter out domain-specific stop words to extract high-value keyphrases

What you need first

Text NormalizationPython strings and text

Lowercasing strings and stripping punctuation with regex ensures that words like 'Battery', 'battery!', and 'battery' aggregate under a single token.

PythonMini example
import re; s = 'Battery!'; print(re.sub(r'[^a-z0-9\s]', '', s.lower()))
battery
Watch out

Splitting on whitespace without stripping punctuation leaves trailing commas and periods attached to words, corrupting frequency tallies.

N-Gram Sliding WindowsSliding window and buffers

Generating consecutive word pairs (bigrams) captures contextual phrases like 'battery life' that single words miss.

PythonMini example
words = ['good', 'battery', 'life']; print([f'{words[i]} {words[i+1]}' for i in range(len(words)-1)])
['good battery', 'battery life']
Watch out

Iterating range(len(words)) without subtracting the n-gram window size raises an IndexError at the final word index.

Defensive Counter IndexingPython hash maps

Calling most_common(1) on an empty Counter returns an empty list []; indexing [0] immediately causes an IndexError unless guarded.

PythonMini example
from collections import Counter; c = Counter(); print(c.most_common(1)[0] if c else ('none', 0))
('none', 0)
Watch out

Assuming most_common(1)[0] is always safe causes unexpected pipeline crashes whenever empty text strings are processed.

Architecture and data flow

Problem scenario diagram for py-proj-word-frequency-analyzer
Problem scenario diagram for py-proj-word-frequency-analyzer. Figure 1: Operational problem and business context
Data flow diagram for py-proj-word-frequency-analyzer
Data flow diagram for py-proj-word-frequency-analyzer. Figure 2: Pipeline execution and data transformation flow
Result shape diagram for py-proj-word-frequency-analyzer
Result shape diagram for py-proj-word-frequency-analyzer. Figure 3: Output verification and method results

The data

Raw customer product review feedback strings containing punctuation, empty entries, and repeated keywords.

customer_reviews: Raw customer review lines

review_idreview_text
REV-01The battery life is amazing. Great battery!
REV-02Poor battery life, but great screen display.
REV-03...
REV-04Amazing screen display and great battery life.

Step by step

Read each step, run its code, then check your output against the one shown.

  1. Clean and tokenize raw customer review strings

    We begin by developing a text cleaning and tokenization function. Raw customer feedback frequently contains inconsistent uppercase letters, trailing punctuation, and extra whitespace. Our tokenizer converts input strings to lowercase and applies a regular expression to extract sequences of alphanumeric characters. We filter out empty tokens to produce a clean list of normalized word strings. We inspect the resulting token list to confirm that punctuation has been cleanly stripped before phrases are extracted by downstream analytics modules. This defensive parsing structure ensures that text processing pipelines process arbitrary document streams without throwing unhandled exceptions or corrupting frequency tallies.

    PythonStep 1
    import re
    
    def tokenize(text: str) -> list[str]:
        cleaned = re.sub(r"[^a-zA-Z0-9\s]", " ", text.lower())
        return [w.strip() for w in cleaned.split() if w.strip()]
    
    sample_review = "The battery life is amazing! Great battery, but poor screen display..."
    tokens = tokenize(sample_review)
    print(f"Extracted {len(tokens)} tokens: {tokens}")
    Extracted 11 tokens: ['the', 'battery', 'life', 'is', 'amazing', 'great', 'battery', 'but', 'poor', 'screen', 'display']
    Common mistake

    Watch out: Using str.split(' ') without regular expression cleaning leaves punctuation attached to tokens, causing 'battery!' and 'battery' to be counted separately.

    How you will know

    The output confirms 11 clean tokens without trailing exclamation marks or ellipses.

  2. Demonstrate IndexError from unguarded empty Counter indexing

    Wrong turn first: this step shows the mistake before the fix

    We demonstrate the exact technical flaw that crashed the production pipeline: indexing the result of Counter.most_common(1)[0] on empty inputs. Calling most_common(n) on an empty Counter does not raise an error; it returns an empty list []. However, attempting to index the first element [0] of that empty list immediately raises an IndexError. In batch pipelines, an empty customer review string causes this unguarded lookup to crash the entire data pipeline and abort processing mid-stream. This defensive parsing structure ensures that text processing pipelines process arbitrary document streams without throwing unhandled exceptions or corrupting frequency tallies.

    PythonStep 2
    from collections import Counter
    
    empty_review = "..."
    tokens = [w for w in empty_review.split() if w.isalpha()]
    counts = Counter(tokens)
    
    print(f"most_common(1) on empty counter returns: {counts.most_common(1)}")
    
    try:
        # Naive unguarded indexing causes crash!
        top = counts.most_common(1)[0]
    except IndexError as e:
        print(f"Caught expected IndexError: {type(e).__name__} - index out of range!")
    most_common(1) on empty counter returns: []
    Caught expected IndexError: IndexError - index out of range!
    Common mistake

    Watch out: Claiming most_common() raises IndexError is technically wrong; most_common() returns an empty list, and the [0] index raises the IndexError.

    How you will know

    The output shows most_common(1) returns [] and indexing [0] raises an IndexError.

  3. Construct n-gram sliding window generator

    Individual words often lack business context. For instance, the token 'battery' alone does not reveal whether the customer loved or hated the battery. Extracting bigrams (two-word phrases) like 'battery life' or 'poor battery' provides actionable sentiment insights. We construct an n-gram generator that iterates across the token list with a sliding window of size n. We inspect generated bigrams to verify sequential phrase extraction across consecutive words in the sentence without out-of-bounds slicing errors. This defensive parsing structure ensures that text processing pipelines process arbitrary document streams without throwing unhandled exceptions or corrupting frequency tallies.

    PythonStep 3
    def get_ngrams(tokens: list[str], n: int = 2) -> list[str]:
        if len(tokens) < n:
            return []
        return [" ".join(tokens[i : i + n]) for i in range(len(tokens) - n + 1)]
    
    tokens = ["the", "battery", "life", "is", "amazing"]
    bigrams = get_ngrams(tokens, n=2)
    print(f"Generated {len(bigrams)} bigrams: {bigrams}")
    Generated 4 bigrams: ['the battery', 'battery life', 'life is', 'is amazing']
    Common mistake

    Watch out: Looping up to range(len(tokens)) without subtracting n + 1 causes slicing bounds to generate incomplete trailing n-grams.

    How you will know

    Output confirms 4 bigrams: ['the battery', 'battery life', 'life is', 'is amazing'].

  4. Filter stop words and aggregate keyphrase frequencies

    Grammatical filler words such as 'the', 'is', 'and', 'but', and 'in' dominate raw frequency counts without carrying meaningful semantic value. We define a set of common stop words and filter them out during phrase generation. We use collections.Counter to aggregate keyphrase frequencies across the cleaned token stream. We inspect frequency counts to verify that informative domain phrases rise to the top of our analytical output rather than grammatical noise that skews executive sentiment summaries. This defensive parsing structure ensures that text processing pipelines process arbitrary document streams without throwing unhandled exceptions or corrupting frequency tallies.

    PythonStep 4
    from collections import Counter
    import re
    
    STOP_WORDS = {"the", "is", "and", "but", "in", "to", "a", "of", "it"}
    
    def clean_tokens(text: str) -> list[str]:
        cleaned = re.sub(r"[^a-zA-Z0-9\s]", " ", text.lower())
        return [w for w in cleaned.split() if w not in STOP_WORDS]
    
    reviews = [
        "The battery life is amazing. Great battery!",
        "Poor battery life, but great screen display.",
        "Amazing screen display and great battery life."
    ]
    
    all_bigrams = []
    for r in reviews:
        tokens = clean_tokens(r)
        # Extract bigrams
        all_bigrams.extend([" ".join(tokens[i : i + 2]) for i in range(len(tokens) - 1)])
    
    counts = Counter(all_bigrams)
    print("Top Keyphrase Frequencies:")
    for phrase, count in counts.most_common(3):
        print(f"  '{phrase}': {count} occurrences")
    Top Keyphrase Frequencies:
      'battery life': 3 occurrences
      'great battery': 2 occurrences
      'screen display': 2 occurrences
    Common mistake

    Watch out: Using a list instead of a set for stop words causes O(n) membership lookup overhead on every word in large corpora.

    How you will know

    Output ranks 'battery life' with 3 occurrences as the top customer keyphrase.

  5. Build resilient text analyzer with defensive top-k readout

    In our final milestone, we build an enterprise TextAnalyzer class that integrates tokenization, stop-word filtering, n-gram extraction, and defensive top-k retrieval. The class accepts arbitrary customer review streams, safely absorbs empty or punctuation-only strings without crashing, and produces an executive summary table showing top keyphrases and percentage shares. We run a diverse batch of reviews including an empty string to verify complete resilience against corrupted production text lines and guarantee continuous reporting throughput. This defensive parsing structure ensures that text processing pipelines process arbitrary document streams without throwing unhandled exceptions or corrupting frequency tallies.

    PythonStep 5
    from collections import Counter
    import re
    
    class TextAnalyzer:
        STOP_WORDS = {"the", "is", "and", "but", "in", "to", "a", "of", "it"}
    
        def __init__(self, n: int = 2):
            self.n = n
            self.counter = Counter()
            self.total_docs = 0
    
        def process(self, text: str) -> None:
            self.total_docs += 1
            cleaned = re.sub(r"[^a-zA-Z0-9\s]", " ", text.lower())
            tokens = [w for w in cleaned.split() if w not in self.STOP_WORDS]
            if len(tokens) >= self.n:
                ngrams = [" ".join(tokens[i : i + self.n]) for i in range(len(tokens) - self.n + 1)]
                self.counter.update(ngrams)
    
        def top_phrases(self, k: int = 3) -> list[tuple[str, int]]:
            # Defensive: most_common() returns [] when counter is empty
            return self.counter.most_common(k)
    
    analyzer = TextAnalyzer(n=2)
    batch = [
        "The battery life is amazing. Great battery!",
        "...",  # Degenerate empty review
        "Poor battery life, but great screen display.",
        "Amazing screen display and great battery life."
    ]
    
    for doc in batch:
        analyzer.process(doc)
    
    print(f"Processed {analyzer.total_docs} reviews cleanly.")
    print("Top Phrases Readout:")
    for phrase, freq in analyzer.top_phrases(3):
        print(f"  {phrase} -> {freq}")
    Processed 4 reviews cleanly.
    Top Phrases Readout:
      battery life -> 3
      great battery -> 2
      screen display -> 2
    Common mistake

    Watch out: Failing to update total_docs on empty inputs skews document frequency percentages in downstream analytics pipelines.

    How you will know

    Output confirms 4 reviews processed cleanly without crashing, listing 'battery life -> 3' as the top phrase.

Build it yourself

Write your solution below, run it in your browser, and pass every check.

Python 3Ready to run
Loading editor…
Press Run Code to evaluate
Text analyzer executes without unhandled exceptions
Processes 4 review documents including empty entries
Extracts battery life as top keyphrase with frequency 3

What the result means

Interpretation

The streaming n-gram text analyzer provides complete resilience against malformed, blank, or punctuation-heavy customer reviews. By eliminating the IndexError on empty Counter lookups and filtering stop-words, the system highlights genuine customer complaint topics like 'battery life' while processing batch inputs uninterrupted.

The message you would send

Hi Marketing Insights Team, the overhauled streaming N-Gram text analyzer has been validated. The new pipeline handles empty or malformed reviews gracefully without crashing, accurately isolating top multi-word keyphrases (such as 'battery life' appearing in 75% of sample reviews) for weekly sentiment reporting.

Put it on your CV

Resume or LinkedIn bullet

OutputCopy this line
Engineered a resilient streaming n-gram text analyzer in Python with regex tokenization and defensive Counter indexing, processing 40,000 weekly customer reviews without pipeline crashes.

README for GitHub

OutputREADME.md
# Streaming N-Gram Frequency Text Analyzer

## Business Context
Weekly customer sentiment aggregation pipelines crashed intermittently upon encountering empty reviews or punctuation-only entries, delaying marketing reports by 3 days.

## Solution Architecture
- Applied regex-based alphanumeric tokenization with lowercasing and stop-word filtering.
- Implemented sliding window n-gram extraction to capture contextual customer phrases.
- Protected `collections.Counter` lookups to eliminate `IndexError` exceptions on empty document batches.

## Verification
Passes all automated verification checks on Python 3 standard library.
Stretch goal

Implement TF-IDF (Term Frequency - Inverse Document Frequency) scoring to weight n-grams by uniqueness across the document corpus.

Keep going