Build a Movie Review Sentiment Analyzer

Train an ML model to classify movie reviews as positive or negative.

Introduction

30 Second Summary

Every time you check a product rating or scroll through reviews before watching a film, you are relying on sentiment analysis. Computers can learn to read text and decide whether the opinion is positive or negative, just like you do.

In this project, you will train a machine learning model that classifies movie reviews as positive or negative. You will build the entire pipeline from raw text to live predictions you can test with your own sentences.

What You'll Build

A command-line sentiment analyzer that predicts whether movie reviews are positive or negative using Logistic Regression trained on 2,000 real movie reviews.

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

  • A Python virtual environment with all ML dependencies isolated from your system.
  • A complete ML pipeline script (sentiment_analyzer.py) that loads data, trains a model, evaluates it, and predicts on new text.
  • A trained classifier achieving ~85% accuracy on held-out test data, with precision, recall, and F1 metrics to prove it.
  • Secret Mission: Pit multiple classifiers against each other and tune hyperparameters with grid search to squeeze out maximum accuracy.

Are there any prerequisites?

You should be comfortable with basic Python syntax including variables, functions, loops, and imports. No prior machine learning experience is needed.

Set Up Your Python Environment

Before you can train a machine learning model, you need an isolated workspace where your ML libraries live without interfering with the rest of your system.

In this step, you will create a project folder, set up a Python virtual environment, and install the two core libraries for this project: scikit-learn for machine learning and NLTK for accessing real movie review data.

In this step, get ready to:
  • Create a project folder and a Python virtual environment.
  • Install scikit-learn and NLTK into your virtual environment.
  • Verify the installation and download the movie reviews corpus.
Create a project folder and set up a virtual environment

A virtual environment keeps your project's packages separate from your system Python. This way, the specific versions of scikit-learn and NLTK you install here won't conflict with anything else on your machine.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type VS Code and press Enter to open it.
  • Open the integrated terminal by pressing Ctrl+` (Windows) or Ctrl+` (macOS).
  • 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
  • Confirm the folder exists by running:
ls

You should see the sentiment-analyzer folder listed if you run ls ~/Desktop. Your terminal prompt should also show you are inside sentiment-analyzer.

  • Create a Python virtual environment by running this command:

macOS

python3 -m venv .venv

Windows

python -m venv .venv
  • Activate the virtual environment by running:

macOS

source .venv/bin/activate

Windows

.venv\Scripts\activate

You should see (.venv) appear at the beginning of your terminal prompt. This confirms the virtual environment is active.

What is a virtual environment?

A virtual environment is an isolated copy of Python that holds its own set of installed packages. When you activate it, commands like pip install only affect that environment.

This means you can have different projects with different library versions on the same machine without conflicts.

Don't see (.venv) in your prompt?

Make sure you ran the activate command for your operating system. On macOS, you need source before the path. On Windows, the path uses backslashes and the Scripts folder instead of bin.

If you see a permissions error on Windows, try running VS Code as administrator.

help me activate my Python virtual environment

Install scikit-learn and NLTK

With your virtual environment active, packages you install go into .venv and nowhere else.

  • Install both ML libraries by running this command:
pip install scikit-learn==1.9.0 nltk==3.9.4

What are these packages?

scikit-learn is Python's most popular library for traditional machine learning. It provides tools for classification, regression, clustering, and model evaluation.

nltk (Natural Language Toolkit) provides access to text datasets and text processing utilities. You will use its built-in movie reviews corpus as your training data.

The installation may take a minute. You should see progress bars and a final line confirming both packages were installed successfully.

Seeing an installation error?

Make sure (.venv) is visible in your prompt before running pip install. If you see a permissions error, you likely forgot to activate the virtual environment.

If you see a network timeout, check your internet connection and try the command again.

help me fix my pip install error

Verify the installation and download the movie reviews corpus

Now you will create the main Python file for this project. This first version will verify that your libraries imported correctly and download the dataset you will train on.

  • In the VS Code Explorer sidebar on the left, click the New File icon (a page with a plus sign).
  • Name the file sentiment_analyzer.py and press Enter.

Don't see the Explorer sidebar?

Click File in the top menu bar, then Open Folder. Navigate to your Desktop and select the sentiment-analyzer folder. The Explorer will then show your project files.

  • In sentiment_analyzer.py, add the following imports at the top of the file:
import re
import nltk
from nltk.corpus import movie_reviews
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
from sklearn.metrics import classification_report, accuracy_score
from sklearn.pipeline import Pipeline

What do these imports do?

re is Python's built-in regular expressions module for text cleaning. nltk gives you access to the movie reviews dataset.

The sklearn imports bring in the tools you will use in later steps: a text vectorizer, a classifier, data splitting, metrics, and a pipeline to chain them together. Importing them now confirms everything installed correctly.

  • Below the imports, add the download_data function and the main block:

def download_data():
    """Download the NLTK movie reviews corpus."""
    nltk.download("movie_reviews", quiet=True)
    print(f"scikit-learn imported successfully")
    print(f"NLTK movie_reviews corpus ready")
    print(f"Categories: {movie_reviews.categories()}")
    print(f"Total files: {len(movie_reviews.fileids())}")


if __name__ == "__main__":
    download_data()

What does this code do?

nltk.download("movie_reviews", quiet=True) fetches the movie reviews corpus from NLTK's data server. The quiet=True flag suppresses download progress messages.

The print statements confirm that scikit-learn imported without errors and that the corpus is ready. The if __name__ == "__main__" block ensures the function only runs when you execute the script directly.

✔️ Awesome, I've got everything!

Great. Double check you have saved your file before running it.

ⓧ I'd like to double check the full code

import re
import nltk
from nltk.corpus import movie_reviews
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
from sklearn.metrics import classification_report, accuracy_score
from sklearn.pipeline import Pipeline


def download_data():
    """Download the NLTK movie reviews corpus."""
    nltk.download("movie_reviews", quiet=True)
    print(f"scikit-learn imported successfully")
    print(f"NLTK movie_reviews corpus ready")
    print(f"Categories: {movie_reviews.categories()}")
    print(f"Total files: {len(movie_reviews.fileids())}")


if __name__ == "__main__":
    download_data()
  • Save sentiment_analyzer.py by pressing Cmd+S (macOS) or Ctrl+S (Windows).
  • Run the script in your terminal by running this command:

macOS

python3 sentiment_analyzer.py

Windows

python sentiment_analyzer.py

You should see output confirming everything is working:

scikit-learn imported successfully
NLTK movie_reviews corpus ready
Categories: ['neg', 'pos']
Total files: 2000

What should I see?

The output confirms three things. First, scikit-learn imported without errors. Second, the NLTK movie reviews corpus downloaded successfully. Third, the corpus contains 2,000 files split across two categories: neg (negative reviews) and pos (positive reviews).

Seeing a ModuleNotFoundError?

This means Python cannot find scikit-learn or NLTK. Check that (.venv) is showing in your terminal prompt. If it is not, re-activate your virtual environment and try again.

Also confirm you are running the script with the same Python that has the packages installed. On macOS, use python3 (not python).

help me fix my ModuleNotFoundError

Your environment is ready with all the ML libraries installed and 2,000 movie reviews waiting to be analyzed. Next up, you will load this dataset and explore its structure.

Load and Explore the Dataset

Your environment is set up with all the tools you need. Now it's time to feed your model some real data.

A machine learning model learns patterns from examples. In this step, you'll load 2,000 real movie reviews from the NLTK corpus and split them into two groups: one for training and one for testing.

In this step, get ready to:
  • Load all 2,000 movie reviews into Python lists paired with their sentiment labels.
  • Print dataset statistics to confirm balanced positive and negative classes.
  • Split the data into training (80%) and testing (20%) sets for model evaluation.
Write the data loading function

The NLTK movie_reviews corpus stores 2,000 reviews organized into two folders: pos and neg. You need a function that reads every review file and pairs its text with the correct label.

  • In sentiment_analyzer.py, find the closing line of the download_data() function.
  • Add the following function below it, before the existing main block:
def load_dataset():
    """Load movie reviews and their labels."""
    documents = []
    labels = []

    for category in movie_reviews.categories():
        for fileid in movie_reviews.fileids(category):
            raw_text = movie_reviews.raw(fileid)
            documents.append(raw_text)
            labels.append(category)

    print(f"\nDataset loaded:")
    print(f"  Total reviews: {len(documents)}")
    print(f"  Positive: {labels.count('pos')}")
    print(f"  Negative: {labels.count('neg')}")
    print(f"\nSample review (first 200 chars):")
    print(f"  {documents[0][:200]}...")
    print(f"  Label: {labels[0]}")

    return documents, labels

What does this code do?

This function loops through every review in both categories and builds two parallel lists.

  • movie_reviews.categories() returns ['neg', 'pos']. The outer loop iterates over each category.
  • movie_reviews.fileids(category) returns all file IDs for that category. The inner loop grabs the raw text of each review.
  • documents holds the review text. labels holds the matching sentiment ('pos' or 'neg'). Each index in one list corresponds to the same index in the other.
  • The print statements give you a quick snapshot of the data to confirm everything loaded correctly.

You won't see output from this function yet. The next chunk updates the main block to call it.

Split the dataset into training and test sets

A model that memorizes its training data looks accurate on paper but fails on new text. To measure real performance, you hold back a portion of data the model never sees during training.

What is a train/test split?

You divide your dataset into two groups. The training set teaches the model patterns. The test set evaluates whether the model learned those patterns well enough to generalize to unseen text.

An 80/20 split is a common starting point. With 2,000 reviews, that gives 1,600 for training and 400 for testing.

  • In sentiment_analyzer.py, find the existing main block at the bottom of the file:
if __name__ == "__main__":
    download_data()
  • Replace the entire main block with this updated version that loads the dataset and splits it:
if __name__ == "__main__":
    download_data()
    documents, labels = load_dataset()
    X_train, X_test, y_train, y_test = train_test_split(
        documents, labels, test_size=0.2, random_state=42, stratify=labels
    )
    print(f"\nTrain/Test split:")
    print(f"  Training samples: {len(X_train)}")
    print(f"  Testing samples: {len(X_test)}")

What does this code do?

  • load_dataset() returns both lists, which you unpack into documents and labels.
  • train_test_split shuffles the data and divides it into four pieces: training inputs, test inputs, training labels, and test labels.
  • test_size=0.2 reserves 20% (400 reviews) for testing.
  • random_state=42 locks the random shuffle so you get the same split every time you run the script. This makes your results reproducible.
  • stratify=labels ensures the positive/negative ratio stays balanced in both the training and test sets.
  • Save sentiment_analyzer.py.
  • Run the script in your terminal by running this command:
python sentiment_analyzer.py
  • You should see output showing 2,000 total reviews (1,000 positive, 1,000 negative), a sample review snippet, and the train/test split confirming 1,600 training and 400 testing samples.

Seeing a NameError or unexpected output?

  • If you see NameError: name 'load_dataset' is not defined, make sure the load_dataset() function is defined above the if __name__ block, not inside it.
  • If you see NameError: name 'train_test_split' is not defined, confirm the import line from sklearn.model_selection import train_test_split is still at the top of your file.
  • If numbers look wrong, check that you're looping through both categories and not just one.

My script runs but the numbers don't match what's expected. Can you help me debug?

✔️ 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 at this point:

import re
import nltk
from nltk.corpus import movie_reviews
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
from sklearn.metrics import classification_report, accuracy_score
from sklearn.pipeline import Pipeline


def download_data():
    """Download the NLTK movie reviews corpus."""
    nltk.download("movie_reviews", quiet=True)
    print(f"scikit-learn imported successfully")
    print(f"NLTK movie_reviews corpus ready")
    print(f"Categories: {movie_reviews.categories()}")
    print(f"Total files: {len(movie_reviews.fileids())}")


def load_dataset():
    """Load movie reviews and their labels."""
    documents = []
    labels = []

    for category in movie_reviews.categories():
        for fileid in movie_reviews.fileids(category):
            raw_text = movie_reviews.raw(fileid)
            documents.append(raw_text)
            labels.append(category)

    print(f"\nDataset loaded:")
    print(f"  Total reviews: {len(documents)}")
    print(f"  Positive: {labels.count('pos')}")
    print(f"  Negative: {labels.count('neg')}")
    print(f"\nSample review (first 200 chars):")
    print(f"  {documents[0][:200]}...")
    print(f"  Label: {labels[0]}")

    return documents, labels


if __name__ == "__main__":
    download_data()
    documents, labels = load_dataset()
    X_train, X_test, y_train, y_test = train_test_split(
        documents, labels, test_size=0.2, random_state=42, stratify=labels
    )
    print(f"\nTrain/Test split:")
    print(f"  Training samples: {len(X_train)}")
    print(f"  Testing samples: {len(X_test)}")

Your data is loaded, explored, and split. Next up, you'll build the machine learning pipeline that transforms raw text into predictions.

Build the ML Pipeline

Your dataset is loaded and split into 1,600 training reviews and 400 test reviews. Now comes the core challenge: turning raw text into something a machine learning model can actually learn from.

In this step, you will build a complete ML pipeline that chains text preprocessing, feature extraction, and classification into a single object. One call to .fit() will handle everything from cleaning the text to training the classifier.

In this step, get ready to:
  • Create a text cleaning function and an ML pipeline combining TF-IDF with Logistic Regression.
  • Train the pipeline on your training data with a single method call.
  • Understand what TF-IDF vectorization does and why pipelines prevent data leakage.
Create the text preprocessing and ML pipeline

Before the model can learn from text, you need to clean it into a consistent format. Numbers, punctuation, and mixed capitalization add noise that confuses the vectorizer.

  • In sentiment_analyzer.py, find the end of the load_dataset() function.
  • Add the following clean_text() function below it:
def clean_text(text):
    """Basic text cleaning: lowercase and remove non-alphabetic characters."""
    text = text.lower()
    text = re.sub(r"[^a-z\s]", "", text)
    return text.strip()

What does this code do?

  • text.lower() converts everything to lowercase so "Great" and "great" are treated as the same word.
  • re.sub(r"[^a-z\s]", "", text) strips out anything that is not a letter or whitespace. This removes punctuation, numbers, and special characters.
  • text.strip() removes any leading or trailing whitespace left over after cleaning.

You will not see a change yet. The next chunk uses this function as a preprocessor inside the pipeline.

Now you need the pipeline itself. A Pipeline in scikit-learn chains multiple processing steps together so data flows through each one in order.

  • Add the following build_pipeline() function directly below clean_text():
def build_pipeline():
    """Create the ML pipeline with TF-IDF and Logistic Regression."""
    pipeline = Pipeline([
        ("tfidf", TfidfVectorizer(
            max_features=5000,
            ngram_range=(1, 2),
            preprocessor=clean_text,
            stop_words="english"
        )),
        ("classifier", LogisticRegression(
            max_iter=1000,
            random_state=42
        ))
    ])
    return pipeline

What does this code do?

This function creates a two-step pipeline:

  • The first step is a TfidfVectorizer named "tfidf". It converts raw text into numerical features. max_features=5000 limits the vocabulary to the 5,000 most important words. ngram_range=(1, 2) captures both single words (unigrams) and two-word phrases (bigrams) like "not good".
  • The second step is a LogisticRegression classifier named "classifier". It learns patterns in the numerical features to predict positive or negative sentiment. max_iter=1000 gives it enough iterations to converge.

The preprocessor=clean_text parameter tells the vectorizer to run your clean_text() function on every review before extracting features.

The stop_words="english" parameter removes common words like "the", "is", and "a" that carry no sentiment information.

You will not see a change yet. The next substep trains the pipeline, which produces visible output.

Train the pipeline on your training data

With the pipeline defined, a single .fit() call handles both vectorization and training in one shot. The pipeline feeds your raw text through the vectorizer first, then passes the resulting numbers to the classifier.

  • In sentiment_analyzer.py, scroll down to the if __name__ == "__main__": block.
  • Find the last two lines that print the train/test split counts:
    print(f"  Training samples: {len(X_train)}")
    print(f"  Testing samples: {len(X_test)}")
  • Add the following code directly below those lines to build, train, and inspect your pipeline:
    pipeline = build_pipeline()
    print("\nTraining the model...")
    pipeline.fit(X_train, y_train)
    print("Training complete!")
    print(pipeline)

What does this code do?

  • build_pipeline() creates the pipeline object with both steps ready.
  • pipeline.fit(X_train, y_train) trains the entire pipeline. Internally, it first vectorizes all 1,600 training reviews into TF-IDF features, then trains the Logistic Regression classifier on those features.
  • print(pipeline) displays the pipeline object so you can see both the tfidf and classifier steps listed with their configurations.
  • Save sentiment_analyzer.py.
  • Run the script in your terminal by running:
python sentiment_analyzer.py

You should see the dataset loading output from before, followed by "Training the model...", "Training complete!", and the pipeline object showing both steps.

Seeing an error during training?

  • If you see NameError: name 'clean_text' is not defined, make sure your clean_text() function is defined above build_pipeline() in the file. Python reads top to bottom.
  • If you see an indentation error, check that the new lines inside __main__ are indented with four spaces (matching the existing code in that block).
  • If the script runs but stops without printing the pipeline, make sure print(pipeline) is the last line in your __main__ block.

Still stuck? Help me debug why my pipeline is not training correctly.

✔️ Awesome, I've got everything!

Great. Double check you have saved your file and that your terminal output shows both pipeline steps listed.

ⓧ I'd like to double check the full code

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

import re
import nltk
from nltk.corpus import movie_reviews
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
from sklearn.metrics import classification_report, accuracy_score
from sklearn.pipeline import Pipeline


def download_data():
    """Download the NLTK movie reviews corpus."""
    nltk.download("movie_reviews", quiet=True)
    print(f"scikit-learn imported successfully")
    print(f"NLTK movie_reviews corpus ready")
    print(f"Categories: {movie_reviews.categories()}")
    print(f"Total files: {len(movie_reviews.fileids())}")


def load_dataset():
    """Load movie reviews and their labels."""
    documents = []
    labels = []

    for category in movie_reviews.categories():
        for fileid in movie_reviews.fileids(category):
            raw_text = movie_reviews.raw(fileid)
            documents.append(raw_text)
            labels.append(category)

    print(f"\nDataset loaded:")
    print(f"  Total reviews: {len(documents)}")
    print(f"  Positive: {labels.count('pos')}")
    print(f"  Negative: {labels.count('neg')}")
    print(f"\nSample review (first 200 chars):")
    print(f"  {documents[0][:200]}...")
    print(f"  Label: {labels[0]}")

    return documents, labels


def clean_text(text):
    """Basic text cleaning: lowercase and remove non-alphabetic characters."""
    text = text.lower()
    text = re.sub(r"[^a-z\s]", "", text)
    return text.strip()


def build_pipeline():
    """Create the ML pipeline with TF-IDF and Logistic Regression."""
    pipeline = Pipeline([
        ("tfidf", TfidfVectorizer(
            max_features=5000,
            ngram_range=(1, 2),
            preprocessor=clean_text,
            stop_words="english"
        )),
        ("classifier", LogisticRegression(
            max_iter=1000,
            random_state=42
        ))
    ])
    return pipeline


if __name__ == "__main__":
    download_data()
    documents, labels = load_dataset()
    X_train, X_test, y_train, y_test = train_test_split(
        documents, labels, test_size=0.2, random_state=42, stratify=labels
    )
    print(f"\nTrain/Test split:")
    print(f"  Training samples: {len(X_train)}")
    print(f"  Testing samples: {len(X_test)}")
    pipeline = build_pipeline()
    print("\nTraining the model...")
    pipeline.fit(X_train, y_train)
    print("Training complete!")
    print(pipeline)
Understand TF-IDF and why pipelines matter

Your model just trained successfully. Let's unpack what happened under the hood.

What is TF-IDF?

TF-IDF stands for Term Frequency-Inverse Document Frequency. It converts words into numbers by weighing how important each word is to a specific document compared to the entire collection.

A word that appears frequently in one review but rarely across all reviews gets a high TF-IDF score. Words like "brilliant" or "awful" score high because they appear in specific reviews. Common words like "the" score near zero because they appear everywhere.

Why use a Pipeline instead of separate steps?

Pipelines prevent data leakage. If you vectorized ALL your data before splitting into train/test, the vectorizer would learn word importance from the test set too. That gives your model an unfair advantage and inflates accuracy scores.

With a pipeline, the vectorizer only ever sees training data during .fit(). When you later predict on test data, the pipeline applies the same vocabulary it learned from training. No leakage.

Your pipeline is trained and ready to make predictions. Next up, you will evaluate how well it actually performs on reviews it has never seen before.

Evaluate Your Model

Your pipeline is trained and ready. Now it's time to find out how well it actually performs on data it has never seen before.

The true test of a machine learning model isn't how well it memorizes training data. It's how accurately it predicts on new, unseen examples. In this step, you'll measure your model's accuracy, examine per-class performance metrics, and test it with your own sentences.

In this step, get ready to:
  • Evaluate the model's accuracy on the 400-sample test set.
  • Print a classification report with precision, recall, and F1-score.
  • Test the model with hand-written example sentences.
Write the model evaluation function

Your model trained on 1,600 reviews. The remaining 400 reviews in X_test are reviews the model has never seen. Predicting on these unseen reviews tells you how well the model generalizes to new data.

  • In sentiment_analyzer.py, find the build_pipeline() function, then add the following evaluate_model() function below it:
def evaluate_model(pipeline, X_test, y_test):
    """Evaluate the trained model on test data."""
    # Generate predictions on the unseen test set
    y_pred = pipeline.predict(X_test)

    # Calculate and print overall accuracy
    accuracy = accuracy_score(y_test, y_pred)
    print(f"\nModel Accuracy: {accuracy:.2%}")
    # Print detailed per-class metrics
    print(f"\nClassification Report:")
    print(classification_report(y_test, y_pred, target_names=["Negative", "Positive"]))

    return accuracy

What does this code do?

pipeline.predict(X_test) feeds all 400 test reviews through the same TF-IDF vectorizer and classifier, producing a predicted label for each one.

accuracy_score() compares predictions to the real labels and returns the fraction that matched. classification_report() goes deeper. It shows precision (of all reviews predicted positive, how many actually were), recall (of all actually positive reviews, how many did the model catch), and F1-score (the harmonic mean of both).

  • Save sentiment_analyzer.py.

Seeing a NameError for accuracy_score?

Make sure the import line from sklearn.metrics import classification_report, accuracy_score is at the top of your file. It was added in an earlier step but may be missing if you started fresh.

Help me fix a NameError in my evaluate_model function

Test with hand-written example sentences

Accuracy on a test set is useful, but nothing beats trying the model on your own words. You'll write two functions: one that predicts sentiment for a single piece of text, and another that feeds several example sentences through it.

  • Below the evaluate_model() function, add the predict_sentiment() helper function:
def predict_sentiment(text, pipeline):
    """Predict sentiment of a single text with confidence."""
    # Run the text through the trained pipeline
    prediction = pipeline.predict([text])[0]
    # Get probability scores for each class
    probabilities = pipeline.predict_proba([text])[0]

    # Convert internal label to readable format
    label = "Positive" if prediction == "pos" else "Negative"
    confidence = max(probabilities)

    return label, confidence

What does this code do?

pipeline.predict([text])[0] wraps the input in a list (the pipeline expects multiple samples) and grabs the first result.

pipeline.predict_proba([text])[0] returns the model's confidence for each class (negative and positive). Taking the max() gives you the confidence for whichever class won.

  • Now add the test_examples() function directly below predict_sentiment():
def test_examples(pipeline):
    """Test the model with hand-written examples."""
    examples = [
        "This movie was absolutely fantastic! The acting was superb.",
        "Terrible film. Worst I have ever seen. Complete waste of time.",
        "The movie was okay, nothing special but not bad either.",
        "A masterpiece of storytelling with incredible performances.",
    ]

    print("\nTesting with example sentences:")
    print("-" * 50)
    for text in examples:
        label, confidence = predict_sentiment(text, pipeline)
        print(f"  Text: {text[:60]}...")
        print(f"  Prediction: {label} (confidence: {confidence:.2%})")
        print()

What does this code do?

This function feeds four carefully chosen sentences through the predictor. The examples cover clearly positive, clearly negative, ambiguous, and strongly positive text. This lets you see how the model handles different levels of certainty.

  • Save sentiment_analyzer.py.

Seeing an IndentationError?

Make sure both predict_sentiment() and test_examples() are defined at the top level (no extra indentation). They should start at the same indent level as evaluate_model().

Help me fix an IndentationError in my sentiment analyzer

Run the evaluation

The functions are defined, but the script doesn't call them yet. You need to wire them into the main block so they run after training completes.

  • Scroll down to the if __name__ == "__main__": block at the bottom of your file.
  • Find this line:
    print(pipeline)
  • Replace print(pipeline) with the two evaluation calls, so the end of the block looks like this:
    evaluate_model(pipeline, X_test, y_test)
    test_examples(pipeline)
  • Save sentiment_analyzer.py.
  • Run the script by entering this command in your terminal:
python sentiment_analyzer.py
  • You should see the model accuracy printed (approximately 82-87%), followed by a classification report table, and then predictions for each of the four example sentences with confidence percentages.

What should I see in the classification report?

The report shows a table with Negative and Positive rows. Each row has precision, recall, and F1-score values between 0 and 1.

Balanced values across both classes (e.g., both around 0.84-0.86) mean the model is equally good at detecting positive and negative reviews. It's not biased toward one class.

Script runs but accuracy is much lower than 80%?

Check that your train_test_split uses random_state=42 and stratify=labels. Without stratify, the train/test split may end up unbalanced, hurting performance.

Help me debug low accuracy in my sentiment analyzer

✔️ 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 full sentiment_analyzer.py at this point:

import re
import nltk
from nltk.corpus import movie_reviews
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
from sklearn.metrics import classification_report, accuracy_score
from sklearn.pipeline import Pipeline


def download_data():
    """Download the NLTK movie reviews corpus."""
    nltk.download("movie_reviews", quiet=True)
    print(f"scikit-learn imported successfully")
    print(f"NLTK movie_reviews corpus ready")
    print(f"Categories: {movie_reviews.categories()}")
    print(f"Total files: {len(movie_reviews.fileids())}")


def load_dataset():
    """Load movie reviews and their labels."""
    documents = []
    labels = []

    for category in movie_reviews.categories():
        for fileid in movie_reviews.fileids(category):
            raw_text = movie_reviews.raw(fileid)
            documents.append(raw_text)
            labels.append(category)

    print(f"\nDataset loaded:")
    print(f"  Total reviews: {len(documents)}")
    print(f"  Positive: {labels.count('pos')}")
    print(f"  Negative: {labels.count('neg')}")
    print(f"\nSample review (first 200 chars):")
    print(f"  {documents[0][:200]}...")
    print(f"  Label: {labels[0]}")

    return documents, labels


def clean_text(text):
    """Basic text cleaning: lowercase and remove non-alphabetic characters."""
    text = text.lower()
    text = re.sub(r"[^a-z\s]", "", text)
    return text.strip()


def build_pipeline():
    """Create the ML pipeline with TF-IDF and Logistic Regression."""
    pipeline = Pipeline([
        ("tfidf", TfidfVectorizer(
            max_features=5000,
            ngram_range=(1, 2),
            preprocessor=clean_text,
            stop_words="english"
        )),
        ("classifier", LogisticRegression(
            max_iter=1000,
            random_state=42
        ))
    ])
    return pipeline


def evaluate_model(pipeline, X_test, y_test):
    """Evaluate the trained model on test data."""
    y_pred = pipeline.predict(X_test)

    accuracy = accuracy_score(y_test, y_pred)
    print(f"\nModel Accuracy: {accuracy:.2%}")
    print(f"\nClassification Report:")
    print(classification_report(y_test, y_pred, target_names=["Negative", "Positive"]))

    return accuracy


def predict_sentiment(text, pipeline):
    """Predict sentiment of a single text with confidence."""
    prediction = pipeline.predict([text])[0]
    probabilities = pipeline.predict_proba([text])[0]

    label = "Positive" if prediction == "pos" else "Negative"
    confidence = max(probabilities)

    return label, confidence


def test_examples(pipeline):
    """Test the model with hand-written examples."""
    examples = [
        "This movie was absolutely fantastic! The acting was superb.",
        "Terrible film. Worst I have ever seen. Complete waste of time.",
        "The movie was okay, nothing special but not bad either.",
        "A masterpiece of storytelling with incredible performances.",
    ]

    print("\nTesting with example sentences:")
    print("-" * 50)
    for text in examples:
        label, confidence = predict_sentiment(text, pipeline)
        print(f"  Text: {text[:60]}...")
        print(f"  Prediction: {label} (confidence: {confidence:.2%})")
        print()


if __name__ == "__main__":
    download_data()
    documents, labels = load_dataset()
    X_train, X_test, y_train, y_test = train_test_split(
        documents, labels, test_size=0.2, random_state=42, stratify=labels
    )
    print(f"\nTrain/Test split:")
    print(f"  Training samples: {len(X_train)}")
    print(f"  Testing samples: {len(X_test)}")
    pipeline = build_pipeline()
    print("\nTraining the model...")
    pipeline.fit(X_train, y_train)
    print("Training complete!")
    evaluate_model(pipeline, X_test, y_test)
    test_examples(pipeline)

Your model can now evaluate itself and predict sentiment on any text you throw at it. Next up, you'll wrap this into an interactive loop so you can type reviews in real-time and see predictions instantly.

Build the Interactive Predictor

Your model can classify reviews and report its confidence. Now it is time to make it interactive so anyone can type a sentence and get an instant prediction.

In this step you will wrap the prediction logic in a continuous input loop and consolidate the entire script into a clean main() function that runs end-to-end from the command line.

In this step, get ready to:
  • Build an interactive input loop that predicts sentiment on demand.
  • Create a main() function that orchestrates the full ML pipeline.
  • Run and test the complete script with your own movie reviews.
Write the interactive predictor function

The predict_sentiment() function you built in the previous step handles a single prediction. Now you need a wrapper that keeps asking the user for input until they decide to stop.

  • In sentiment_analyzer.py, find the test_examples() function. Add the following new function directly below it:
def interactive_predictor(pipeline):
    """Run an interactive loop for sentiment prediction."""
    print("\n" + "=" * 50)
    print("INTERACTIVE SENTIMENT PREDICTOR")
    print("=" * 50)
    print("Type a movie review and press Enter to get a prediction.")
    print("Type 'quit' to exit.\n")

    while True:
        user_input = input("Enter text: ").strip()
        if user_input.lower() == "quit":
            print("Goodbye!")
            break
        if not user_input:
            print("Please enter some text.\n")
            continue

        label, confidence = predict_sentiment(user_input, pipeline)
        print(f"  -> {label} (confidence: {confidence:.2%})\n")

What does this code do?

This function creates a REPL-style loop (Read-Eval-Print Loop) for your model.

  • input("Enter text: ").strip() waits for the user to type something and removes extra whitespace.
  • The "quit" check gives users a clean way to exit the loop.
  • Empty input is caught so the model does not try to predict on a blank string.
  • Each prediction calls predict_sentiment() and prints the label with its confidence percentage.
  • Save sentiment_analyzer.py.
Create the main() function

Right now the script's orchestration logic sits directly under if __name__ == "__main__":. Wrapping it in a main() function makes the code reusable and easier to extend.

  • In sentiment_analyzer.py, scroll to the bottom and find the if __name__ == "__main__": block.
  • Delete everything from if __name__ == "__main__": down to the end of the file.
  • Replace it with the following main() function and entry point:
def main():
    # Step 1: Download and verify data
    download_data()

    # Step 2: Load and explore dataset
    documents, labels = load_dataset()

    # Step 3: Split into train/test
    X_train, X_test, y_train, y_test = train_test_split(
        documents, labels, test_size=0.2, random_state=42, stratify=labels
    )
    print(f"\nTrain/Test split:")
    print(f"  Training samples: {len(X_train)}")
    print(f"  Testing samples: {len(X_test)}")

    # Step 4: Build and train pipeline
    pipeline = build_pipeline()
    print("\nTraining the model...")
    pipeline.fit(X_train, y_train)
    print("Training complete!")

    # Step 5: Evaluate
    evaluate_model(pipeline, X_test, y_test)

    # Step 6: Test with examples
    test_examples(pipeline)

    # Step 7: Interactive predictor
    interactive_predictor(pipeline)


if __name__ == "__main__":
    main()

What does this code do?

The main() function is the single entry point that runs your entire ML pipeline from start to finish.

  • It calls each function in order: download data, load reviews, split, build pipeline, train, evaluate, test examples, and finally launch the interactive predictor.
  • The if __name__ == "__main__": guard ensures main() only runs when you execute the script directly, not when importing it as a module.
  • Save sentiment_analyzer.py.

Seeing a NameError?

Make sure the interactive_predictor() function definition appears above main() in the file. Python reads top to bottom, so calling a function before it is defined causes a NameError.

Also confirm you deleted the old if __name__ block completely. Having two of them will cause unexpected behavior.

Help me fix a NameError in my sentiment analyzer script.

✔️ Awesome, I've got everything!

Great. Double check you have saved your file before running the script.

ⓧ I'd like to double check the full code

Here is the complete sentiment_analyzer.py file at this point:

import re
import nltk
from nltk.corpus import movie_reviews
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
from sklearn.metrics import classification_report, accuracy_score
from sklearn.pipeline import Pipeline


def download_data():
    """Download the NLTK movie reviews corpus."""
    nltk.download("movie_reviews", quiet=True)
    print(f"scikit-learn imported successfully")
    print(f"NLTK movie_reviews corpus ready")
    print(f"Categories: {movie_reviews.categories()}")
    print(f"Total files: {len(movie_reviews.fileids())}")


def load_dataset():
    """Load movie reviews and their labels."""
    documents = []
    labels = []

    for category in movie_reviews.categories():
        for fileid in movie_reviews.fileids(category):
            raw_text = movie_reviews.raw(fileid)
            documents.append(raw_text)
            labels.append(category)

    print(f"\nDataset loaded:")
    print(f"  Total reviews: {len(documents)}")
    print(f"  Positive: {labels.count('pos')}")
    print(f"  Negative: {labels.count('neg')}")
    print(f"\nSample review (first 200 chars):")
    print(f"  {documents[0][:200]}...")
    print(f"  Label: {labels[0]}")

    return documents, labels


def clean_text(text):
    """Basic text cleaning: lowercase and remove non-alphabetic characters."""
    text = text.lower()
    text = re.sub(r"[^a-z\s]", "", text)
    return text.strip()


def build_pipeline():
    """Create the ML pipeline with TF-IDF and Logistic Regression."""
    pipeline = Pipeline([
        ("tfidf", TfidfVectorizer(
            max_features=5000,
            ngram_range=(1, 2),
            preprocessor=clean_text,
            stop_words="english"
        )),
        ("classifier", LogisticRegression(
            max_iter=1000,
            random_state=42
        ))
    ])
    return pipeline


def evaluate_model(pipeline, X_test, y_test):
    """Evaluate the trained model on test data."""
    y_pred = pipeline.predict(X_test)

    accuracy = accuracy_score(y_test, y_pred)
    print(f"\nModel Accuracy: {accuracy:.2%}")
    print(f"\nClassification Report:")
    print(classification_report(y_test, y_pred, target_names=["Negative", "Positive"]))

    return accuracy


def predict_sentiment(text, pipeline):
    """Predict sentiment of a single text with confidence."""
    prediction = pipeline.predict([text])[0]
    probabilities = pipeline.predict_proba([text])[0]

    label = "Positive" if prediction == "pos" else "Negative"
    confidence = max(probabilities)

    return label, confidence


def test_examples(pipeline):
    """Test the model with hand-written examples."""
    examples = [
        "This movie was absolutely fantastic! The acting was superb.",
        "Terrible film. Worst I have ever seen. Complete waste of time.",
        "The movie was okay, nothing special but not bad either.",
        "A masterpiece of storytelling with incredible performances.",
    ]

    print("\nTesting with example sentences:")
    print("-" * 50)
    for text in examples:
        label, confidence = predict_sentiment(text, pipeline)
        print(f"  Text: {text[:60]}...")
        print(f"  Prediction: {label} (confidence: {confidence:.2%})")
        print()


def interactive_predictor(pipeline):
    """Run an interactive loop for sentiment prediction."""
    print("\n" + "=" * 50)
    print("INTERACTIVE SENTIMENT PREDICTOR")
    print("=" * 50)
    print("Type a movie review and press Enter to get a prediction.")
    print("Type 'quit' to exit.\n")

    while True:
        user_input = input("Enter text: ").strip()
        if user_input.lower() == "quit":
            print("Goodbye!")
            break
        if not user_input:
            print("Please enter some text.\n")
            continue

        label, confidence = predict_sentiment(user_input, pipeline)
        print(f"  -> {label} (confidence: {confidence:.2%})\n")


def main():
    # Step 1: Download and verify data
    download_data()

    # Step 2: Load and explore dataset
    documents, labels = load_dataset()

    # Step 3: Split into train/test
    X_train, X_test, y_train, y_test = train_test_split(
        documents, labels, test_size=0.2, random_state=42, stratify=labels
    )
    print(f"\nTrain/Test split:")
    print(f"  Training samples: {len(X_train)}")
    print(f"  Testing samples: {len(X_test)}")

    # Step 4: Build and train pipeline
    pipeline = build_pipeline()
    print("\nTraining the model...")
    pipeline.fit(X_train, y_train)
    print("Training complete!")

    # Step 5: Evaluate
    evaluate_model(pipeline, X_test, y_test)

    # Step 6: Test with examples
    test_examples(pipeline)

    # Step 7: Interactive predictor
    interactive_predictor(pipeline)


if __name__ == "__main__":
    main()
Run and test the complete script

Time to see the full pipeline in action. Running the script executes every stage from data download through to the interactive predictor.

  • Run your script by executing this command in the terminal:
python sentiment_analyzer.py

What should I see?

The script runs through all stages automatically: downloading data, loading reviews, splitting, training, and printing the classification report. After the example predictions, you will see the interactive predictor banner.

The Enter text: prompt appears and waits for your input.

  • Try typing an enthusiastic review like:
  • This film blew me away with stunning visuals and a gripping plot.
  • Try a negative review like:
  • Boring and predictable with terrible acting throughout.
  • Try a neutral statement like:
  • The movie had some good parts and some bad parts.
  • Type quit when you are finished experimenting.

Why does confidence vary?

Strong, opinionated language ("fantastic", "terrible") gives the model high confidence because those words appeared frequently in one category during training. Neutral or mixed language produces lower confidence because the model sees features from both classes.

This is the difference between a confident prediction and an uncertain one. Both are valuable signals.

Script hangs or nothing prints?

If the script appears frozen, it is likely waiting at the Enter text: prompt. Scroll up in your terminal to see if the training output already printed above.

If you see an ImportError, make sure your virtual environment is still active. You should see (.venv) at the start of your terminal prompt.

Help me debug my sentiment analyzer script that is not running correctly.

Your sentiment analyzer is complete. You have a fully working ML pipeline that loads data, trains a model, evaluates performance, and predicts on demand. Next up, the Secret Mission challenges you to pit multiple classifiers against each other and squeeze out maximum accuracy.

Secret mission

Classifier Showdown with Hyperparameter Tuning

Your Logistic Regression model works well, but is it the best option? In this secret mission, you'll pit multiple classifiers against each other and use grid search to systematically find the optimal hyperparameters for maximum accuracy.

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 in place to continue experimenting or delete them to reclaim disk space.

Resources you used:

  • Project folder on your Desktop containing sentiment_analyzer.py and the .venv virtual environment.
  • Python packages (scikit-learn and NLTK) installed inside the virtual environment.
  • Downloaded NLTK movie_reviews corpus data.

Keep everything running

No action needed. Choose this if you're still actively experimenting with the model or want to try different classifiers and hyperparameters.

  • Your virtual environment and all installed packages stay in place.
  • You can rerun sentiment_analyzer.py anytime by activating the virtual environment and running the script.
  • The NLTK corpus data remains on your system for use in other NLP projects.

Pause - I'll come back to this later

Close your terminal and VS Code. No background processes are running from this project, so there is nothing to shut down.

  • Close any terminal windows you used for this project.
  • Close VS Code if you opened it for this project.
  • Your files and virtual environment remain on your Desktop, ready for when you come back.

Delete - I don't want to use this again

Remove the project folder and all its contents. This deletes your script, the virtual environment, and all installed packages.

macOS

  • Open your terminal and remove the project folder by running:
cd ~/Desktop
rm -rf sentiment-analyzer
  • Remove the downloaded NLTK corpus data by running:
rm -rf ~/nltk_data/corpora/movie_reviews

Windows

  • Open your terminal and remove the project folder by running:
cd ~\Desktop
rmdir /s /q sentiment-analyzer
  • Remove the downloaded NLTK corpus data by running:
rmdir /s /q %APPDATA%\nltk_data\corpora\movie_reviews
  • Confirm the folder is gone by checking your Desktop. You should no longer see the sentiment-analyzer folder.

Nice Work!

Nice Work!

You just built a working sentiment analysis model from scratch. Starting with raw movie reviews and ending with an interactive predictor, you now understand the full machine learning pipeline pattern that powers real-world text classification systems.

You've learned how to:

  • Build a complete ML pipeline that loads 2,000 movie reviews, extracts numerical features with TF-IDF vectorization, and trains a Logistic Regression classifier achieving approximately 85% accuracy.
  • Evaluate model performance using precision, recall, and F1 metrics on held-out test data to understand how well the model generalizes.
  • Build an interactive prediction function that takes any text input, runs it through the trained pipeline, and returns a sentiment label with a confidence score.
  • Secret Mission: Compared multiple classifiers (Logistic Regression, Naive Bayes, Linear SVC) and tuned hyperparameters with GridSearchCV to squeeze out maximum accuracy.

Ready to quiz yourself?