Movie Review Sentiment Analyzer

Train a Naive Bayes classifier to predict movie review sentiment.

Introduction

30 Second Summary

Every time you scroll past a star rating or skim a product review, your brain is doing sentiment analysis. You read a few words and instantly know whether someone loved or hated something. Computers can learn to do the same thing by spotting patterns in language.

In this project, you will train a machine learning model to read movie reviews and predict whether they are positive or negative. You will build the entire pipeline from raw text to real-time predictions.

What You'll Build

A command-line sentiment analyzer that classifies movie reviews as positive or negative using a Naive Bayes classifier trained on 2,000 real movie reviews from the NLTK corpus.

By the end of this project, you'll have:

  • A trained Naive Bayes classifier that predicts movie review sentiment with ~80%+ accuracy on held-out test data.
  • A clear understanding of how TF-IDF converts raw English text into numbers that a machine learning model can process.
  • An interactive predictor where you type any sentence and the model judges it in real-time with a confidence score.
  • Secret Mission: Train a second classifier and compare its performance head-to-head against Naive Bayes to see which algorithm wins.

Are there any prerequisites?

You should be comfortable with Python basics like variables, functions, loops, and lists. No prior experience with NLP or text classification is needed.

Load and Explore the Data

Your environment is ready with all the tools installed. Now it's time to get your hands on some real data.

Before any machine learning model can learn, it needs training data. You'll load 2,000 real movie reviews from the NLTK corpus and prepare them for your classifier.

In this step, get ready to:
  • Load 2,000 movie reviews from the NLTK corpus into Python lists.
  • Explore the dataset to understand its structure and balance.
  • Split the data into training and test sets for model evaluation.
Create your project folder and file

You need a dedicated folder for this project and a Python file to hold all your code.

  • Open your terminal in VS Code by pressing Ctrl+` (macOS and Windows).
  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir sentiment-analyzer
cd sentiment-analyzer
  • Create the Python file by running:
touch sentiment_analyzer.py
  • Confirm the file exists by running:
ls
  • You should see sentiment_analyzer.py in the output.
  • Open the sentiment-analyzer folder in VS Code by clicking File in the top menu bar.
  • Click Open Folder.
  • Select the sentiment-analyzer folder on your Desktop, then click Open.
  • Open sentiment_analyzer.py by clicking it in the Explorer sidebar on the left.
Load the movie reviews corpus

The NLTK movie reviews corpus contains 1,000 positive and 1,000 negative reviews. You'll load them all into two Python lists: one for the review text and one for the labels.

  • Add all the imports your project needs to the top of sentiment_analyzer.py by copying this code:
import random
from nltk.corpus import movie_reviews
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.model_selection import train_test_split
from sklearn.naive_bayes import MultinomialNB
from sklearn.metrics import accuracy_score, classification_report
import numpy as np

Why import everything upfront?

You're importing libraries you won't use until later steps (like TfidfVectorizer and MultinomialNB). Putting all imports at the top of the file is a Python best practice. It makes dependencies visible at a glance.

  • Below the imports, add the code that loads the movie reviews and builds your data lists:
# ============================================================
# STEP 2: Load and Explore the Data
# ============================================================

# Get all file IDs for each category
positive_ids = movie_reviews.fileids('pos')
negative_ids = movie_reviews.fileids('neg')

# Build lists of review texts and labels
reviews = []
labels = []

for file_id in positive_ids:
    reviews.append(movie_reviews.raw(file_id))
    labels.append('positive')

for file_id in negative_ids:
    reviews.append(movie_reviews.raw(file_id))
    labels.append('negative')

What does this code do?

movie_reviews.fileids('pos') returns the filenames of all positive reviews in the corpus. movie_reviews.raw(file_id) returns the full text of a single review as a string.

The two for loops walk through every file ID, grab the raw text, and append it to the reviews list. The matching label ('positive' or 'negative') goes into the labels list at the same index.

  • Save sentiment_analyzer.py.
  • You won't see output yet. The next chunk adds print statements to confirm the data loaded correctly.

Seeing a LookupError about movie_reviews?

This means the NLTK corpus wasn't downloaded in Step 1. Run this in your terminal to fix it:

python -c "import nltk; nltk.download('movie_reviews')"

Then try running your script again.

Help me fix this NLTK download error.

Print dataset statistics

Before training a model, you should always inspect the data. Printing basic stats confirms the data loaded correctly and reveals whether the classes are balanced.

  • Add this code below your for loops in sentiment_analyzer.py:
# Print basic stats
print("=" * 50)
print("DATASET OVERVIEW")
print("=" * 50)
print(f"Total reviews: {len(reviews)}")
print(f"Positive reviews: {len(positive_ids)}")
print(f"Negative reviews: {len(negative_ids)}")
print(f"\nSample review (first 200 chars):")
print(reviews[0][:200] + "...")
print()

What does this code do?

These print statements give you a quick overview of the dataset. You'll see the total count, the split between positive and negative reviews, and a peek at what one review looks like.

The reviews[0][:200] slice grabs the first 200 characters of the first review. This lets you eyeball the data without flooding your terminal.

  • Save sentiment_analyzer.py.
  • Run the script in your terminal by running:
python sentiment_analyzer.py
  • You should see a DATASET OVERVIEW banner showing 2000 total reviews, 1000 positive, and 1000 negative. A snippet of the first review appears below the counts.

Don't see any output?

Make sure you saved the file before running. Check that your terminal is in the sentiment-analyzer folder where your file lives.

If you see a syntax error, double-check that the f-strings use curly braces {len(reviews)} and that every print() call has matching parentheses.

Help me debug my sentiment analyzer script.

Shuffle and split the data

A model trained on all positive reviews followed by all negative reviews would learn the order, not the content. Shuffling prevents this. Splitting into training and test sets lets you measure how well the model generalizes to reviews it has never seen.

  • Add this code below the print() statement at the end of your file:
# Shuffle and split the data
X_train, X_test, y_train, y_test = train_test_split(
    reviews, labels, test_size=0.2, random_state=42, stratify=labels
)

print(f"Training set size: {len(X_train)}")
print(f"Test set size: {len(X_test)}")
print()

What does this code do?

train_test_split from scikit-learn shuffles your data and splits it into two groups. 80% goes to training (what the model learns from) and 20% goes to testing (what you evaluate it against).

random_state=42 fixes the random seed so you get the same split every time you run the script. stratify=labels ensures both the training and test sets have the same ratio of positive to negative reviews.

  • Save sentiment_analyzer.py and run the script again:
python sentiment_analyzer.py
  • Below the dataset overview, you should now see Training set size: 1600 and Test set size: 400.

Seeing different numbers?

If your training set isn't 1600 or your test set isn't 400, check that test_size=0.2 is set correctly. 20% of 2000 is 400.

If you see a NameError about train_test_split, make sure your import line reads from sklearn.model_selection import train_test_split at the top of the file.

Help me fix my train_test_split error.

✔️ Awesome, I've got everything!

Great. Double check you've saved sentiment_analyzer.py before moving on.

ⓧ I'd like to double check the full code

Here's the complete sentiment_analyzer.py at the end of this step:

import random
from nltk.corpus import movie_reviews
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.model_selection import train_test_split
from sklearn.naive_bayes import MultinomialNB
from sklearn.metrics import accuracy_score, classification_report
import numpy as np

# ============================================================
# STEP 2: Load and Explore the Data
# ============================================================

# Get all file IDs for each category
positive_ids = movie_reviews.fileids('pos')
negative_ids = movie_reviews.fileids('neg')

# Build lists of review texts and labels
reviews = []
labels = []

for file_id in positive_ids:
    reviews.append(movie_reviews.raw(file_id))
    labels.append('positive')

for file_id in negative_ids:
    reviews.append(movie_reviews.raw(file_id))
    labels.append('negative')

# Print basic stats
print("=" * 50)
print("DATASET OVERVIEW")
print("=" * 50)
print(f"Total reviews: {len(reviews)}")
print(f"Positive reviews: {len(positive_ids)}")
print(f"Negative reviews: {len(negative_ids)}")
print(f"\nSample review (first 200 chars):")
print(reviews[0][:200] + "...")
print()

# Shuffle and split the data
X_train, X_test, y_train, y_test = train_test_split(
    reviews, labels, test_size=0.2, random_state=42, stratify=labels
)

print(f"Training set size: {len(X_train)}")
print(f"Test set size: {len(X_test)}")
print()

Your data is loaded, explored, and split. Next up, you'll transform these raw English reviews into numbers that a machine learning model can actually understand.

Transform Text into Numbers (TF-IDF)

Your data is loaded and split into training and test sets. Now comes a fundamental challenge: machine learning models only understand numbers, not words.

In this step, you'll convert raw review text into numerical vectors using TF-IDF (Term Frequency-Inverse Document Frequency). TF-IDF scores each word by how important it is to a specific review compared to the entire collection of reviews.

In this step, get ready to:
  • Transform review text into TF-IDF numerical vectors.
  • Inspect the resulting matrix shapes and vocabulary.
  • Examine how individual words get weighted in a real review.
Create a TF-IDF vectorizer and transform the data

A TfidfVectorizer scans your text and builds a vocabulary of words. It then scores each word in each review based on two factors: how often the word appears in that review, and how rare it is across all reviews. Common words like "movie" score low. Distinctive words like "breathtaking" score high.

  • In sentiment_analyzer.py, scroll to the bottom of your file (below the last print() statement).
  • Add the following code to create the vectorizer and transform both training and test data:
# ============================================================
# STEP 3: Transform Text into Numbers (TF-IDF)
# ============================================================

vectorizer = TfidfVectorizer(stop_words='english', max_features=5000)

# fit_transform on training data, transform on test data
X_train_tfidf = vectorizer.fit_transform(X_train)
X_test_tfidf = vectorizer.transform(X_test)

What does this code do?

  • stop_words='english' tells the vectorizer to ignore common English words like "the", "is", and "and" that carry no sentiment signal.
  • max_features=5000 limits the vocabulary to the 5,000 most informative words. This keeps the model fast without losing important signal.
  • fit_transform(X_train) learns the vocabulary from training data AND converts it to numbers in one step.
  • transform(X_test) converts test data using the same vocabulary. It does NOT re-learn the vocabulary, which prevents data leakage.

Why fit_transform on training but only transform on test?

The vectorizer needs to learn its vocabulary (which 5,000 words to track) from the training data only. If it learned from the test data too, the model would have "seen" test information during training.

This is called data leakage. It inflates accuracy during evaluation but means the model performs worse on truly unseen text.

  • Save sentiment_analyzer.py.
Inspect the matrix shapes and vocabulary

The vectorizer produces a sparse matrix where each row is a review and each column is a word from the vocabulary. Printing the shape and some vocabulary samples helps you see what TF-IDF actually produced.

  • Add this code directly below the previous block to print matrix details and vocabulary samples:
print("=" * 50)
print("TF-IDF VECTORIZATION")
print("=" * 50)
print(f"Training matrix shape: {X_train_tfidf.shape}")
print(f"  (that's {X_train_tfidf.shape[0]} reviews, each represented as {X_train_tfidf.shape[1]} features)")
print()

# Show some feature names
feature_names = vectorizer.get_feature_names_out()
print(f"Sample vocabulary words: {list(feature_names[:10])}")
print(f"More words: {list(feature_names[2500:2510])}")
print()

What does this code do?

  • X_train_tfidf.shape shows the dimensions of the matrix. You'll see something like (1600, 5000), meaning 1,600 reviews each represented by 5,000 word scores.
  • get_feature_names_out() returns the actual words the vectorizer chose for its vocabulary. These are the 5,000 words it considers most informative.
  • Save sentiment_analyzer.py and run the script in your terminal:
python sentiment_analyzer.py
  • In the output, look for the "TF-IDF VECTORIZATION" section. You should see a matrix shape of (1600, 5000) and two lists of vocabulary words printed.

Not seeing the TF-IDF section in output?

  • Make sure you added the new code below the Step 2 code, not inside it.
  • Check that vectorizer is defined before you try to call get_feature_names_out() on it.
  • If you see a NameError, confirm that X_train_tfidf is assigned before the print statements.

Why isn't my TF-IDF vectorization printing any output?

Print TF-IDF scores for a sample review

The matrix shape tells you the size, but seeing actual word scores makes TF-IDF click. Words unique to a review get high scores. Words that appear in many reviews get low scores.

  • Add this code below the vocabulary print statements to display the top-scoring words in the first training review:
# Show TF-IDF scores for one review
sample_vector = X_train_tfidf[0]
non_zero_indices = sample_vector.nonzero()[1]
print("Top TF-IDF words in the first training review:")
word_scores = [(feature_names[i], sample_vector[0, i]) for i in non_zero_indices]
word_scores.sort(key=lambda x: x[1], reverse=True)
for word, score in word_scores[:10]:
    print(f"  {word}: {score:.4f}")
print()

What does this code do?

  • sample_vector = X_train_tfidf[0] grabs the TF-IDF vector for the first training review.
  • nonzero()[1] finds which word columns have a score greater than zero (most are zero because each review only uses a fraction of the 5,000-word vocabulary).
  • The sort and slice show you the top 10 highest-scoring words. These are the words TF-IDF considers most "distinctive" for this particular review.
  • Save sentiment_analyzer.py and run the script again:
python sentiment_analyzer.py
  • You should now see a list of 10 words with decimal scores. Words with higher scores (closer to 0.3 or 0.4) are more distinctive to that review.

Seeing an IndexError or empty output?

  • Make sure feature_names is defined above this block (from the previous chunk). The variable is shared between both sections.
  • Check that you're using sample_vector[0, i] with a comma (not sample_vector[0][i]). Sparse matrices use this comma syntax for indexing.

Help me debug my TF-IDF score printing code.

✔️ Awesome, I've got everything!

Great. Double check you've saved your file before moving on.

ⓧ I'd like to double check the full code

Here's the complete sentiment_analyzer.py up to this point:

import random
from nltk.corpus import movie_reviews
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.model_selection import train_test_split
from sklearn.naive_bayes import MultinomialNB
from sklearn.metrics import accuracy_score, classification_report
import numpy as np

# ============================================================
# STEP 2: Load and Explore the Data
# ============================================================

positive_ids = movie_reviews.fileids('pos')
negative_ids = movie_reviews.fileids('neg')

reviews = []
labels = []

for file_id in positive_ids:
    reviews.append(movie_reviews.raw(file_id))
    labels.append('positive')

for file_id in negative_ids:
    reviews.append(movie_reviews.raw(file_id))
    labels.append('negative')

print("=" * 50)
print("DATASET OVERVIEW")
print("=" * 50)
print(f"Total reviews: {len(reviews)}")
print(f"Positive reviews: {len(positive_ids)}")
print(f"Negative reviews: {len(negative_ids)}")
print(f"\nSample review (first 200 chars):")
print(reviews[0][:200] + "...")
print()

X_train, X_test, y_train, y_test = train_test_split(
    reviews, labels, test_size=0.2, random_state=42, stratify=labels
)

print(f"Training set size: {len(X_train)}")
print(f"Test set size: {len(X_test)}")
print()

# ============================================================
# STEP 3: Transform Text into Numbers (TF-IDF)
# ============================================================

vectorizer = TfidfVectorizer(stop_words='english', max_features=5000)

# fit_transform on training data, transform on test data
X_train_tfidf = vectorizer.fit_transform(X_train)
X_test_tfidf = vectorizer.transform(X_test)

print("=" * 50)
print("TF-IDF VECTORIZATION")
print("=" * 50)
print(f"Training matrix shape: {X_train_tfidf.shape}")
print(f"  (that's {X_train_tfidf.shape[0]} reviews, each represented as {X_train_tfidf.shape[1]} features)")
print()

# Show some feature names
feature_names = vectorizer.get_feature_names_out()
print(f"Sample vocabulary words: {list(feature_names[:10])}")
print(f"More words: {list(feature_names[2500:2510])}")
print()

# Show TF-IDF scores for one review
sample_vector = X_train_tfidf[0]
non_zero_indices = sample_vector.nonzero()[1]
print("Top TF-IDF words in the first training review:")
word_scores = [(feature_names[i], sample_vector[0, i]) for i in non_zero_indices]
word_scores.sort(key=lambda x: x[1], reverse=True)
for word, score in word_scores[:10]:
    print(f"  {word}: {score:.4f}")
print()

Your text is now a matrix of numbers that a machine learning model can understand. Next up, you'll train a Naive Bayes classifier on these TF-IDF vectors to predict whether a review is positive or negative.

Train the Naive Bayes Classifier

Your text is now represented as numbers. Each review is a row of 5,000 TF-IDF scores, ready for a machine learning algorithm to find patterns in.

In this step, you'll train a Naive Bayes classifier to learn which words appear more often in positive reviews versus negative reviews. Then you'll test it on reviews it has never seen and measure how well it performs.

In this step, get ready to:
  • Train a MultinomialNB classifier on your TF-IDF training data.
  • Predict sentiment on held-out test reviews and measure accuracy.
  • Print a full classification report with precision, recall, and F1-score.
Create and train the Naive Bayes classifier

Naive Bayes works by calculating how likely each word is to appear in a positive review versus a negative review. When it sees a new review, it multiplies those probabilities together to make a prediction.

  • In sentiment_analyzer.py, scroll to the bottom of your file (after the TF-IDF scores loop).
  • Add the following code to create and train your classifier:
# ============================================================
# STEP 4: Train the Naive Bayes Classifier
# ============================================================

print("=" * 50)
print("MODEL TRAINING & EVALUATION")
print("=" * 50)

clf = MultinomialNB()
clf.fit(X_train_tfidf, y_train)

What does this code do?

MultinomialNB() creates a fresh Naive Bayes classifier designed for counting-style features like word frequencies.

clf.fit(X_train_tfidf, y_train) trains the model. It learns the probability of each word appearing in positive versus negative reviews. Training on 1,600 reviews with 5,000 features happens in under a second.

Why Naive Bayes for text classification?

MultinomialNB is one of the two classic Naive Bayes variants used in text classification. It works well with TF-IDF features, requires no hyperparameter tuning, and trains almost instantly.

The "naive" part means it assumes each word contributes independently to the prediction. That's not perfectly true in English, but the simplification works surprisingly well in practice.

Evaluate the model's predictions

The model has learned from training data. Now you need to test it on reviews it has never seen and measure how accurate its predictions are.

  • Add the following code directly below the training code you just wrote:
y_pred = clf.predict(X_test_tfidf)

accuracy = accuracy_score(y_test, y_pred)
print(f"Accuracy: {accuracy:.4f} ({accuracy*100:.1f}%)")
print()
print("Classification Report:")
print(classification_report(y_test, y_pred))

What does this code do?

  • clf.predict(X_test_tfidf) feeds all 400 test reviews through the model and gets a predicted label ("positive" or "negative") for each one.
  • accuracy_score(y_test, y_pred) compares the predicted labels against the actual labels and returns the percentage the model got right.
  • classification_report(y_test, y_pred) prints a detailed breakdown with precision, recall, and F1-score for each class.

What are precision, recall, and F1?

  • Precision: Of the reviews the model labelled positive, what fraction actually were positive?
  • Recall: Of all actually positive reviews, what fraction did the model find?
  • F1-score: A single number that balances precision and recall. Higher is better.
  • Save sentiment_analyzer.py.
  • Run the script in your terminal by running:
python sentiment_analyzer.py
  • You should see an accuracy around 80% or higher, followed by a classification report table showing precision, recall, and F1-score for both the positive and negative classes.

Seeing an accuracy below 50%?

  • Make sure you passed X_train_tfidf and y_train to clf.fit() (not the raw text lists).
  • Check that clf.predict() receives X_test_tfidf (the transformed test data), not X_train_tfidf.
  • Confirm that accuracy_score takes y_test first, then y_pred second (true labels come first).

Why is my model accuracy low?

✔️ Awesome, I've got everything!

Great. Double check you've saved your file before moving on.

ⓧ I'd like to double check the full code

Here is the full sentiment_analyzer.py up to this point:

import random
from nltk.corpus import movie_reviews
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.model_selection import train_test_split
from sklearn.naive_bayes import MultinomialNB
from sklearn.metrics import accuracy_score, classification_report
import numpy as np

# ============================================================
# STEP 2: Load and Explore the Data
# ============================================================

positive_ids = movie_reviews.fileids('pos')
negative_ids = movie_reviews.fileids('neg')

reviews = []
labels = []

for file_id in positive_ids:
    reviews.append(movie_reviews.raw(file_id))
    labels.append('positive')

for file_id in negative_ids:
    reviews.append(movie_reviews.raw(file_id))
    labels.append('negative')

print("=" * 50)
print("DATASET OVERVIEW")
print("=" * 50)
print(f"Total reviews: {len(reviews)}")
print(f"Positive reviews: {len(positive_ids)}")
print(f"Negative reviews: {len(negative_ids)}")
print(f"\nSample review (first 200 chars):")
print(reviews[0][:200] + "...")
print()

X_train, X_test, y_train, y_test = train_test_split(
    reviews, labels, test_size=0.2, random_state=42, stratify=labels
)

print(f"Training set size: {len(X_train)}")
print(f"Test set size: {len(X_test)}")
print()

# ============================================================
# STEP 3: Transform Text into Numbers (TF-IDF)
# ============================================================

vectorizer = TfidfVectorizer(stop_words='english', max_features=5000)

X_train_tfidf = vectorizer.fit_transform(X_train)
X_test_tfidf = vectorizer.transform(X_test)

print("=" * 50)
print("TF-IDF VECTORIZATION")
print("=" * 50)
print(f"Training matrix shape: {X_train_tfidf.shape}")
print(f"  (that's {X_train_tfidf.shape[0]} reviews, each represented as {X_train_tfidf.shape[1]} features)")
print()

feature_names = vectorizer.get_feature_names_out()
print(f"Sample vocabulary words: {list(feature_names[:10])}")
print(f"More words: {list(feature_names[2500:2510])}")
print()

sample_vector = X_train_tfidf[0]
non_zero_indices = sample_vector.nonzero()[1]
print("Top TF-IDF words in the first training review:")
word_scores = [(feature_names[i], sample_vector[0, i]) for i in non_zero_indices]
word_scores.sort(key=lambda x: x[1], reverse=True)
for word, score in word_scores[:10]:
    print(f"  {word}: {score:.4f}")
print()

# ============================================================
# STEP 4: Train the Naive Bayes Classifier
# ============================================================

print("=" * 50)
print("MODEL TRAINING & EVALUATION")
print("=" * 50)

clf = MultinomialNB()
clf.fit(X_train_tfidf, y_train)

y_pred = clf.predict(X_test_tfidf)

accuracy = accuracy_score(y_test, y_pred)
print(f"Accuracy: {accuracy:.4f} ({accuracy*100:.1f}%)")
print()
print("Classification Report:")
print(classification_report(y_test, y_pred))

Your model can now predict sentiment on unseen reviews. Next up, you'll dig into the model's brain to discover which specific words are the strongest signals for positive and negative sentiment.

Discover the Most Predictive Words

Your model is trained and you know its accuracy. But accuracy alone doesn't tell you why the model makes the predictions it does.

In this step, you'll peek inside the trained Naive Bayes model to discover which words are the strongest signals of positive or negative sentiment. These are the words the model relies on most when making predictions.

In this step, get ready to:
  • Extract the log probabilities that the model learned during training.
  • Calculate which words are most indicative of each sentiment class.
  • Print the top 15 positive and top 15 negative indicator words.
Extract log probabilities and compute the difference

Naive Bayes stores a log probability for every word in the vocabulary, per class. A word's log probability tells you how likely that word is to appear in a positive review vs. a negative review.

By subtracting one class's probabilities from the other, you get a single number for each word. A high positive number means "strong positive signal." A low negative number means "strong negative signal."

  • In sentiment_analyzer.py, find the last line of your Step 4 code:
print(classification_report(y_test, y_pred))
  • Add the following code below that line to extract the model's internal probabilities and compute the difference between classes:
# ============================================================
# STEP 5: Discover the Most Predictive Words
# ============================================================

print("=" * 50)
print("MOST PREDICTIVE WORDS")
print("=" * 50)

# Get log probabilities for each class
# clf.classes_ shows the order: ['negative', 'positive']
log_probs = clf.feature_log_prob_

# Calculate the difference: positive log prob - negative log prob
# Higher difference = more indicative of positive
neg_index = list(clf.classes_).index('negative')
pos_index = list(clf.classes_).index('positive')

log_prob_diff = log_probs[pos_index] - log_probs[neg_index]

What does this code do?

The clf.feature_log_prob_ attribute holds the log probabilities the model learned during training. It's a matrix with one row per class (negative, positive) and one column per word in the vocabulary.

By subtracting the negative row from the positive row, you get a single score per word. Words with a high positive score appear much more in positive reviews. Words with a large negative score appear much more in negative reviews.

Why use log probabilities instead of raw probabilities?

Individual word probabilities are incredibly tiny numbers (like 0.00003). Multiplying many tiny numbers together causes floating-point underflow. Logarithms convert multiplication into addition, keeping numbers in a manageable range.

The ranking stays the same whether you compare raw probabilities or their logs. The model uses logs internally for numerical stability, and you can interpret them the same way.

Print the top positive and negative words

Now that you have a score for every word, you can sort them to find the extremes. The words at the top of the sorted list are the strongest positive indicators. The words at the bottom are the strongest negative indicators.

  • Add this code directly below the log_prob_diff line to sort and print the top words for each class:
# Most positive words (highest difference)
top_positive_indices = np.argsort(log_prob_diff)[-15:][::-1]
print("\nTop 15 words indicating POSITIVE sentiment:")
for i in top_positive_indices:
    print(f"  {feature_names[i]}")

# Most negative words (lowest difference)
top_negative_indices = np.argsort(log_prob_diff)[:15]
print("\nTop 15 words indicating NEGATIVE sentiment:")
for i in top_negative_indices:
    print(f"  {feature_names[i]}")
print()

What does this code do?

The np.argsort(log_prob_diff) function returns the indices that would sort the array from lowest to highest.

  • For positive words, you grab the last 15 indices ([-15:]) and reverse them ([::-1]) so the strongest signal comes first.
  • For negative words, you grab the first 15 indices ([:15]) since they already have the lowest (most negative) scores.
  • Save sentiment_analyzer.py.
  • Run the script in your terminal:
python sentiment_analyzer.py
  • Scroll down past the accuracy report. You should see a "MOST PREDICTIVE WORDS" section with two lists of 15 words each.

What should I see?

You'll see words like "bad", "worst", and "waste" in the negative list. The positive list will have words like "excellent", "perfect", and "wonderful". These are the words the model relies on most heavily when making its predictions.

Not seeing the predictive words section?

Make sure you added the new code below the classification_report print statement, not inside or above it.

Check that your indentation is correct. The log_probs line and everything below it should be at the top level (no indentation), not inside the Step 4 code.

Help me debug why my predictive words aren't printing.

Inspect the results

Take a moment to look at the two word lists. Notice how the model's top picks align with your intuition about movie reviews.

  • Look at the negative list. Words like "bad" and "worst" are obvious. But you might also see words like "unfortunately" or "boring" that are more subtle signals.
  • Look at the positive list. Beyond "great" and "excellent", you might spot words that surprise you.

Why do some surprising words appear?

The model doesn't understand meaning. It only knows word frequencies. A word that appears often in positive reviews but rarely in negative ones will rank as a strong positive signal, even if a human wouldn't consider it a "positive" word.

This is both a strength and a limitation of bag-of-words models. They capture statistical patterns humans might miss, but they also miss context and sarcasm entirely.

✔️ Awesome, I've got everything!

Great. Double check you've saved your file before moving on.

ⓧ I'd like to double check the full code

Here's the complete sentiment_analyzer.py up to this point:

import random
from nltk.corpus import movie_reviews
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.model_selection import train_test_split
from sklearn.naive_bayes import MultinomialNB
from sklearn.metrics import accuracy_score, classification_report
import numpy as np

# ============================================================
# STEP 2: Load and Explore the Data
# ============================================================

positive_ids = movie_reviews.fileids('pos')
negative_ids = movie_reviews.fileids('neg')

reviews = []
labels = []

for file_id in positive_ids:
    reviews.append(movie_reviews.raw(file_id))
    labels.append('positive')

for file_id in negative_ids:
    reviews.append(movie_reviews.raw(file_id))
    labels.append('negative')

print("=" * 50)
print("DATASET OVERVIEW")
print("=" * 50)
print(f"Total reviews: {len(reviews)}")
print(f"Positive reviews: {len(positive_ids)}")
print(f"Negative reviews: {len(negative_ids)}")
print(f"\nSample review (first 200 chars):")
print(reviews[0][:200] + "...")
print()

X_train, X_test, y_train, y_test = train_test_split(
    reviews, labels, test_size=0.2, random_state=42, stratify=labels
)

print(f"Training set size: {len(X_train)}")
print(f"Test set size: {len(X_test)}")
print()

# ============================================================
# STEP 3: Transform Text into Numbers (TF-IDF)
# ============================================================

vectorizer = TfidfVectorizer(stop_words='english', max_features=5000)

X_train_tfidf = vectorizer.fit_transform(X_train)
X_test_tfidf = vectorizer.transform(X_test)

print("=" * 50)
print("TF-IDF VECTORIZATION")
print("=" * 50)
print(f"Training matrix shape: {X_train_tfidf.shape}")
print(f"  (that's {X_train_tfidf.shape[0]} reviews, each represented as {X_train_tfidf.shape[1]} features)")
print()

feature_names = vectorizer.get_feature_names_out()
print(f"Sample vocabulary words: {list(feature_names[:10])}")
print(f"More words: {list(feature_names[2500:2510])}")
print()

sample_vector = X_train_tfidf[0]
non_zero_indices = sample_vector.nonzero()[1]
print("Top TF-IDF words in the first training review:")
word_scores = [(feature_names[i], sample_vector[0, i]) for i in non_zero_indices]
word_scores.sort(key=lambda x: x[1], reverse=True)
for word, score in word_scores[:10]:
    print(f"  {word}: {score:.4f}")
print()

# ============================================================
# STEP 4: Train the Naive Bayes Classifier
# ============================================================

print("=" * 50)
print("MODEL TRAINING & EVALUATION")
print("=" * 50)

clf = MultinomialNB()
clf.fit(X_train_tfidf, y_train)

y_pred = clf.predict(X_test_tfidf)

accuracy = accuracy_score(y_test, y_pred)
print(f"Accuracy: {accuracy:.4f} ({accuracy*100:.1f}%)")
print()
print("Classification Report:")
print(classification_report(y_test, y_pred))

# ============================================================
# STEP 5: Discover the Most Predictive Words
# ============================================================

print("=" * 50)
print("MOST PREDICTIVE WORDS")
print("=" * 50)

# Get log probabilities for each class
# clf.classes_ shows the order: ['negative', 'positive']
log_probs = clf.feature_log_prob_

# Calculate the difference: positive log prob - negative log prob
# Higher difference = more indicative of positive
neg_index = list(clf.classes_).index('negative')
pos_index = list(clf.classes_).index('positive')

log_prob_diff = log_probs[pos_index] - log_probs[neg_index]

# Most positive words (highest difference)
top_positive_indices = np.argsort(log_prob_diff)[-15:][::-1]
print("\nTop 15 words indicating POSITIVE sentiment:")
for i in top_positive_indices:
    print(f"  {feature_names[i]}")

# Most negative words (lowest difference)
top_negative_indices = np.argsort(log_prob_diff)[:15]
print("\nTop 15 words indicating NEGATIVE sentiment:")
for i in top_negative_indices:
    print(f"  {feature_names[i]}")
print()

You now know exactly which words drive your model's predictions. Next up, you'll build an interactive mode where you can type any sentence and watch the model judge it in real time.

Build the Interactive Predictor

You've discovered which words are the strongest sentiment signals in your dataset. Now it's time to put everything together into something you can actually play with.

In this step, you'll build an interactive loop that lets you type any sentence and get an instant sentiment prediction with a confidence score. This is where your trained model goes from a set of statistics to a tool you can use.

In this step, get ready to:
  • Write prediction logic that transforms user input and returns a sentiment with confidence.
  • Wrap the logic in an interactive while loop that keeps running until the user types 'quit'.
  • Test the model on different sentences to see where it succeeds and where it gets confused.
Add the interactive prediction loop

Your model is trained and evaluated. The final piece is a loop that accepts text from you, runs it through the same TF-IDF vectorizer, and prints a prediction.

  • In sentiment_analyzer.py, scroll to the very bottom of the file (after the Step 5 code that prints the most predictive words).
  • Add the interactive predictor by copying this code at the end of the file:
# ============================================================
# STEP 6: Interactive Predictor
# ============================================================

print("=" * 50)
print("INTERACTIVE SENTIMENT PREDICTOR")
print("=" * 50)
print("Type a movie review and I'll predict if it's positive or negative.")
print("Type 'quit' to exit.\n")

while True:
    user_input = input("Your review: ")
    if user_input.lower() == 'quit':
        print("Goodbye!")
        break

    # Transform the input using the fitted vectorizer
    input_tfidf = vectorizer.transform([user_input])

    # Predict
    prediction = clf.predict(input_tfidf)[0]
    probabilities = clf.predict_proba(input_tfidf)[0]

    # Get confidence
    confidence = max(probabilities) * 100

    print(f"  Prediction: {prediction.upper()}")
    print(f"  Confidence: {confidence:.1f}%")
    print()

What does this code do?

This block creates a loop that keeps asking for input until the user types 'quit'.

  • vectorizer.transform([user_input]) converts the typed text into the same TF-IDF number format the model was trained on. The text goes in a list because the vectorizer expects a collection of documents.
  • clf.predict() returns the predicted label ('positive' or 'negative').
  • clf.predict_proba() returns probability scores for each class. The highest probability becomes the confidence percentage.
  • Save sentiment_analyzer.py.

Seeing a syntax error?

  • Make sure the while True: line has no extra indentation. It should be at the top level (no spaces before it).
  • Check that the f-strings use curly braces correctly. Each {prediction.upper()} and {confidence:.1f} needs matching opening and closing braces.
  • Ensure the code is pasted after the final print() from Step 5, not inside it.

Help me fix a syntax error in my interactive predictor loop.

✔️ Awesome, I've got everything!

Great. Double check you've saved your file before running it.

ⓧ I'd like to double check the full code

Here is the complete sentiment_analyzer.py file. Compare yours against this:

import random
from nltk.corpus import movie_reviews
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.model_selection import train_test_split
from sklearn.naive_bayes import MultinomialNB
from sklearn.metrics import accuracy_score, classification_report
import numpy as np

# ============================================================
# STEP 2: Load and Explore the Data
# ============================================================

# Get all file IDs for each category
positive_ids = movie_reviews.fileids('pos')
negative_ids = movie_reviews.fileids('neg')

# Build lists of review texts and labels
reviews = []
labels = []

for file_id in positive_ids:
    reviews.append(movie_reviews.raw(file_id))
    labels.append('positive')

for file_id in negative_ids:
    reviews.append(movie_reviews.raw(file_id))
    labels.append('negative')

# Print basic stats
print("=" * 50)
print("DATASET OVERVIEW")
print("=" * 50)
print(f"Total reviews: {len(reviews)}")
print(f"Positive reviews: {len(positive_ids)}")
print(f"Negative reviews: {len(negative_ids)}")
print(f"\nSample review (first 200 chars):")
print(reviews[0][:200] + "...")
print()

# Shuffle and split the data
X_train, X_test, y_train, y_test = train_test_split(
    reviews, labels, test_size=0.2, random_state=42, stratify=labels
)

print(f"Training set size: {len(X_train)}")
print(f"Test set size: {len(X_test)}")
print()

# ============================================================
# STEP 3: Transform Text into Numbers (TF-IDF)
# ============================================================

vectorizer = TfidfVectorizer(stop_words='english', max_features=5000)

# fit_transform on training data, transform on test data
X_train_tfidf = vectorizer.fit_transform(X_train)
X_test_tfidf = vectorizer.transform(X_test)

print("=" * 50)
print("TF-IDF VECTORIZATION")
print("=" * 50)
print(f"Training matrix shape: {X_train_tfidf.shape}")
print(f"  (that's {X_train_tfidf.shape[0]} reviews, each represented as {X_train_tfidf.shape[1]} features)")
print()

# Show some feature names
feature_names = vectorizer.get_feature_names_out()
print(f"Sample vocabulary words: {list(feature_names[:10])}")
print(f"More words: {list(feature_names[2500:2510])}")
print()

# Show TF-IDF scores for one review
sample_vector = X_train_tfidf[0]
non_zero_indices = sample_vector.nonzero()[1]
print("Top TF-IDF words in the first training review:")
word_scores = [(feature_names[i], sample_vector[0, i]) for i in non_zero_indices]
word_scores.sort(key=lambda x: x[1], reverse=True)
for word, score in word_scores[:10]:
    print(f"  {word}: {score:.4f}")
print()

# ============================================================
# STEP 4: Train the Naive Bayes Classifier
# ============================================================

print("=" * 50)
print("MODEL TRAINING & EVALUATION")
print("=" * 50)

clf = MultinomialNB()
clf.fit(X_train_tfidf, y_train)

y_pred = clf.predict(X_test_tfidf)

accuracy = accuracy_score(y_test, y_pred)
print(f"Accuracy: {accuracy:.4f} ({accuracy*100:.1f}%)")
print()
print("Classification Report:")
print(classification_report(y_test, y_pred))

# ============================================================
# STEP 5: Discover the Most Predictive Words
# ============================================================

print("=" * 50)
print("MOST PREDICTIVE WORDS")
print("=" * 50)

# Get log probabilities for each class
# clf.classes_ shows the order: ['negative', 'positive']
log_probs = clf.feature_log_prob_

# Calculate the difference: positive log prob - negative log prob
# Higher difference = more indicative of positive
neg_index = list(clf.classes_).index('negative')
pos_index = list(clf.classes_).index('positive')

log_prob_diff = log_probs[pos_index] - log_probs[neg_index]

# Most positive words (highest difference)
top_positive_indices = np.argsort(log_prob_diff)[-15:][::-1]
print("\nTop 15 words indicating POSITIVE sentiment:")
for i in top_positive_indices:
    print(f"  {feature_names[i]}")

# Most negative words (lowest difference)
top_negative_indices = np.argsort(log_prob_diff)[:15]
print("\nTop 15 words indicating NEGATIVE sentiment:")
for i in top_negative_indices:
    print(f"  {feature_names[i]}")
print()

# ============================================================
# STEP 6: Interactive Predictor
# ============================================================

print("=" * 50)
print("INTERACTIVE SENTIMENT PREDICTOR")
print("=" * 50)
print("Type a movie review and I'll predict if it's positive or negative.")
print("Type 'quit' to exit.\n")

while True:
    user_input = input("Your review: ")
    if user_input.lower() == 'quit':
        print("Goodbye!")
        break

    # Transform the input using the fitted vectorizer
    input_tfidf = vectorizer.transform([user_input])

    # Predict
    prediction = clf.predict(input_tfidf)[0]
    probabilities = clf.predict_proba(input_tfidf)[0]

    # Get confidence
    confidence = max(probabilities) * 100

    print(f"  Prediction: {prediction.upper()}")
    print(f"  Confidence: {confidence:.1f}%")
    print()
Run your sentiment analyzer end-to-end

The script will print all the training output from the earlier steps first, then drop you into the interactive prompt. This takes a few seconds because it loads the corpus, trains the model, and runs the evaluation before reaching the interactive section.

  • Run the full script by entering this command in your terminal:
python sentiment_analyzer.py
  • Wait for all the output from Steps 2-5 to scroll by. You should eventually see the INTERACTIVE SENTIMENT PREDICTOR banner followed by a Your review: prompt waiting for your input.

Script stops with an error before reaching the interactive prompt?

  • If you see a NameError, check that your new code is below all the previous steps. The interactive predictor relies on vectorizer and clf being defined earlier in the file.
  • If you see an IndentationError, make sure the code inside the while True: block is indented consistently with 4 spaces.

Help me debug an error in my sentiment analyzer script.

Test the model with different sentences

The fun part. Try typing sentences that range from clearly positive to clearly negative, then throw in some tricky ones to see how the model handles ambiguity.

  • At the Your review: prompt, type a clearly positive sentence like This movie was absolutely brilliant and I loved every minute and press **Enter**.
  • Try a clearly negative sentence like Terrible acting and a boring plot that went nowhere.
  • Now try something ambiguous like The movie was not bad actually quite enjoyable. Watch how the confidence score drops when the model is less certain.
  • Try a single word like masterpiece or garbage to see how the model handles minimal input.
  • When you're done experimenting, type quit to exit the loop.

Why does the model struggle with some sentences?

The model treats each word independently. It has no understanding of negation, sarcasm, or context.

A sentence like "not bad" contains both "not" and "bad." The model sees "bad" as a negative signal and may get confused. This is a known limitation of bag-of-words approaches. More advanced models (like those using word order or attention) handle these patterns better.

Secret mission

Model Showdown

Your Naive Bayes classifier works well, but is it the best option? Train a second classifier (Logistic Regression) on the same data and pit them against each other. Discover which algorithm wins on this dataset and where they disagree on tricky, ambiguous sentences.

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely locally, so there are no ongoing costs. Decide whether to keep your files and packages installed, or remove them to free up disk space.

Resources you used:

  • sentiment_analyzer.py (your project script).
  • Python packages: scikit-learn, nltk, matplotlib.
  • NLTK movie_reviews corpus (downloaded data files).

Keep everything running

No action needed. Choose this if you want to keep experimenting with your sentiment analyzer or extend it further.

  • Your sentiment_analyzer.py file stays where it is.
  • The installed packages (scikit-learn, nltk, matplotlib) remain available for other Python projects.
  • The NLTK data stays downloaded and ready if you want to explore other NLP tasks.

Pause - I'll come back to this later

There are no running processes to stop. Your script only runs when you execute it. Everything is already paused by default.

  • Keep your files and packages installed so you can pick up where you left off.
  • Nothing is consuming memory or CPU while the script is not running.

Delete - I don't want to use this again

Remove all project files and uninstall the packages you installed for this project.

  • Delete your project file by running:
rm sentiment_analyzer.py
  • Uninstall the Python packages you installed for this project by running:
pip uninstall scikit-learn nltk matplotlib -y
  • Find where NLTK stored its data by running:
python -c "import nltk; print(nltk.data.path)"
  • Delete the NLTK data folder shown in the output. On macOS/Linux:
rm -rf ~/nltk_data
  • On Windows, open File Explorer and delete the nltk_data folder shown in the path output above.

Nice Work!

Nice Work!

You just built your first NLP machine learning pipeline from scratch. Starting from raw English text, you trained a model that predicts whether a movie review is positive or negative.

You've learned how to:

  • Convert raw text into numerical features using TF-IDF vectorization, then train a Naive Bayes classifier that predicts sentiment with ~80%+ accuracy.
  • Evaluate a machine learning model using proper ML metrics (precision, recall, F1-score) and discover which words are the strongest sentiment signals.
  • Build an interactive predictor that classifies any sentence you type in real-time using a trained model.
  • Compared two different ML algorithms (Naive Bayes vs Logistic Regression) head-to-head to see which performs better on text data.

Ready to quiz yourself?