Who Is the Best Batman? Data Decides
Rank every Batman actor using real ratings, box office data, and NLP sentiment.
Introduction
30 Second Summary
Everyone has an opinion on who the best Batman is. But can you back yours up with data?
In this project, you will collect real movie ratings, box office numbers, and audience reviews for every live-action Batman film. You will use sentiment analysis and a weighted scoring model to let the data crown the definitive best Batman.
What You'll Build
A Python data pipeline that queries movie APIs, analyzes audience sentiment, and produces a ranked scorecard of every Batman actor.
By the end of this project, you'll have:
- A data collection script that pulls ratings and box office data from the OMDb API and audience reviews from the TMDB API.
- A sentiment analysis pipeline that scores audience reviews for each Batman actor using TextBlob.
- A weighted scoring model that combines five data dimensions into a composite "Batman Score" per actor, visualized as a ranked bar chart with matplotlib.
- Secret Mission: Add a "Batman Eras" timeline visualization showing how film quality has evolved across decades.
Are there any prerequisites?
You need basic Python knowledge (variables, functions, loops). No prior data science or machine learning experience is required.
Before We Start
Before We Start...
Set Up Your Python Environment
Your Batman classifier needs several tools working together. Python runs the code, pandas handles the data, TextBlob analyzes audience sentiment, and matplotlib creates the final chart.
You also need real movie data to analyze. In this step, you will install Python, set up your project environment, and get free API keys from two movie databases.
In this step, get ready to:
- Install Python 3.11+ and verify it works.
- Create a project folder with a virtual environment and install all packages.
- Get free API keys from OMDb and TMDB.
Check your Python version
This project requires Python 3.11 or higher because pandas 3.0.3 (the version you will install) requires it.
- Open your terminal by pressing Cmd+Space (macOS) or the Windows key (Windows), then type Terminal (macOS) or PowerShell (Windows) and press Enter.
- Check your Python version by running this command:
macOS
python3 --version
Windows
python --version
✔️ I see version 3.11 or higher
Python is ready to go. Move on to the next substep.
ⓧ I see an older version
macOS ships with an older Python version by default. Even if you install a newer version, the python3 command may still point to the old one.
- Download the latest Python 3.11+ installer from python.org/downloads.
- Run the installer and follow the prompts.
- Close and reopen your terminal, then run the version check command again.
- If it still shows the older version, try running python3.11 --version (macOS) to confirm the new install is available. Use python3.11 in place of python3 for the rest of this project.
ⓧ Command not found
Python is not installed on your machine yet.
- Go to python.org/downloads and download the latest Python 3.11+ installer for your operating system.
- Run the installer. On Windows, check the box that says Add Python to PATH before clicking Install Now.
- Close and reopen your terminal after installation.
- Run the version check command again to confirm it works.
Create your project folder and install packages
A dedicated project folder keeps your Batman classifier separate from everything else on your machine. A virtual environment ensures the packages you install do not interfere with other Python projects.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and move into it by running these commands:
mkdir batman-classifier
cd batman-classifier
- Confirm the folder exists by running:
pwd
You should see a path ending in batman-classifier.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Visual Studio Code and press Enter to open it.
- In VS Code, click File in the top menu bar.
- Click Open Folder.
- Navigate to Desktop and select the batman-classifier folder, then click Open.
- Open the integrated terminal by pressing Ctrl+` (both macOS and Windows).
Why use VS Code's terminal?
VS Code's integrated terminal opens directly inside your project folder. This means every command you run from here will target the right directory without extra navigation.
- Create a 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 Python installation inside your project folder. Packages you install here stay contained to this project and do not affect your system-wide Python.
- Install all required packages by running this command:
pip install pandas==3.0.3 textblob==0.20.0 matplotlib==3.10.9 requests==2.34.2
What are these packages?
pandas organizes your movie data into tables. textblob performs sentiment analysis on audience reviews. matplotlib creates the final ranking chart.
requests handles all the API calls to fetch movie data from OMDb and TMDB.
The installation may take a minute. When it finishes, you should see a Successfully installed message listing all four packages.
Seeing an error during install?
Make sure your virtual environment is active. You should see (venv) at the start of your terminal prompt. If not, run the activate command again.
If you see an error about Python version compatibility, confirm you are running Python 3.11+ inside your venv by running python --version.
Help me debug my pip install error.
Download TextBlob data and get your API keys
TextBlob needs language data (called corpora) to analyze text sentiment. You also need API keys to access movie data from OMDb and TMDB.
- Download the TextBlob corpora by running this command:
python -m textblob.download_corpora
You should see several download progress lines ending with a confirmation that the corpora are installed.
Download failing or timing out?
This command downloads data from the internet. Check your internet connection and try again. If you are behind a firewall or proxy, you may need to adjust your network settings.
Help me fix the TextBlob corpora download.
Now you need two free API keys. The first is from OMDb (Open Movie Database), which provides ratings and box office data. The second is from TMDB (The Movie Database), which provides audience review text.
- Open omdbapi.com/apikey.aspx in your browser.
- Select the Free account type (1,000 daily requests).
- Enter your email address and a description (anything is fine, such as batman project).
- Click Submit.
- Check your email inbox for a message from OMDb containing your API key. Copy this key and save it somewhere safe.
What is an API key?
An API key is a unique string that identifies your requests to a service. It tells OMDb and TMDB that you are allowed to access their data. Think of it like a library card that grants you access to their movie database.
- Open themoviedb.org/signup in your browser and create a free account.
- Verify your email address by clicking the link TMDB sends you.
- Log in, then click your profile icon in the top right corner.
- Select Settings.
- In the left sidebar, click API.
- Click the link to request an API key. Select Developer as the type.
- Accept the terms of use and fill in the application details. You can enter any URL for the application URL field.
- Submit the form. Copy your API Read Access Token (the longer token) and save it somewhere safe.
Keep both keys handy
Save both your OMDb API key and your TMDB API Read Access Token in a text file or note-taking app. You will paste them into your Python script in the next step.
Not receiving the OMDb email?
Check your spam folder. The email sometimes takes a few minutes to arrive. If it does not appear after 5 minutes, try submitting the form again with the same email address.
Help me get my API keys set up.
Your environment is ready. You have Python, all the packages, and both API keys. Next up, you will write the code that pulls real Batman film data from OMDb.
Collect Batman Film Data from OMDb
Your environment is set up and your API keys are ready. Now it's time to start pulling real movie data.
In this step, you'll create the main Python script and use the OMDb API to collect ratings, review scores, and box office numbers for every live-action Batman film ever made.
In this step, get ready to:
- Define the complete list of Batman films with their IMDb IDs and actors.
- Write functions that query the OMDb API and extract key data fields.
- Run the data collection and verify all 11 films are captured in a DataFrame.
Create the file and define the Batman film dataset
Every data project starts with defining what data you need. For this classifier, you need a structured list of every live-action Batman film paired with the actor who played Batman.
- In VS Code, create a new file called batman_classifier.py in your project folder.
- Add the following imports, API configuration, and film data by pasting this code:
import requests
import pandas as pd
from textblob import TextBlob
import matplotlib.pyplot as plt
import numpy as np
# ============================================================
# CONFIGURATION
# ============================================================
OMDB_API_KEY = "your-omdb-api-key-here"
TMDB_API_KEY = "your-tmdb-api-key-here"
# Every live-action Batman film mapped to actor and TMDB ID
BATMAN_FILMS = [
{"title": "Batman", "year": 1966, "imdb_id": "tt0060153", "tmdb_id": 2661, "actor": "Adam West"},
{"title": "Batman", "year": 1989, "imdb_id": "tt0096895", "tmdb_id": 268, "actor": "Michael Keaton"},
{"title": "Batman Returns", "year": 1992, "imdb_id": "tt0103776", "tmdb_id": 364, "actor": "Michael Keaton"},
{"title": "Batman Forever", "year": 1995, "imdb_id": "tt0112462", "tmdb_id": 414, "actor": "Val Kilmer"},
{"title": "Batman & Robin", "year": 1997, "imdb_id": "tt0118688", "tmdb_id": 415, "actor": "George Clooney"},
{"title": "Batman Begins", "year": 2005, "imdb_id": "tt0372784", "tmdb_id": 272, "actor": "Christian Bale"},
{"title": "The Dark Knight", "year": 2008, "imdb_id": "tt0468569", "tmdb_id": 155, "actor": "Christian Bale"},
{"title": "The Dark Knight Rises", "year": 2012, "imdb_id": "tt1345836", "tmdb_id": 49026, "actor": "Christian Bale"},
{"title": "Batman v Superman: Dawn of Justice", "year": 2016, "imdb_id": "tt2975590", "tmdb_id": 209112, "actor": "Ben Affleck"},
{"title": "Justice League", "year": 2017, "imdb_id": "tt0974015", "tmdb_id": 141052, "actor": "Ben Affleck"},
{"title": "The Batman", "year": 2022, "imdb_id": "tt1877830", "tmdb_id": 414906, "actor": "Robert Pattinson"},
]
What does this code do?
The imports bring in the libraries you installed in Step 1. You won't use all of them yet, but defining them now keeps the file organized.
OMDB_API_KEY and TMDB_API_KEY store your API credentials as constants at the top of the file for easy reference.
BATMAN_FILMS is a list of dictionaries, one per film. Each dictionary maps the film to its IMDb ID (for OMDb lookups), TMDB ID (for reviews later), and the actor who played Batman.
- Replace your-omdb-api-key-here with the OMDb API key you received by email.
- Replace your-tmdb-api-key-here with your TMDB Read Access Token from your TMDB account settings.
- Save batman_classifier.py.
Not sure where to find your API keys?
Your OMDb key was emailed to the address you used when signing up at omdbapi.com. Check your spam folder if you don't see it.
Your TMDB Read Access Token is in your TMDB account under Settings then API. Copy the long token labeled "Read Access Token", not the shorter API Key.
help me find my OMDb or TMDB API key
Write the OMDb query function
The OMDb API returns a JSON object for each film containing its IMDb rating, Rotten Tomatoes score, Metacritic score, and box office gross. You need a function to call the API and extract these fields cleanly.
- Below the BATMAN_FILMS list, add the get_omdb_data function by pasting this code:
# ============================================================
# STEP 1: COLLECT FILM DATA FROM OMDb
# ============================================================
def get_omdb_data(imdb_id):
"""Query OMDb API for a single film by IMDb ID."""
url = f"http://www.omdbapi.com/?i={imdb_id}&apikey={OMDB_API_KEY}"
response = requests.get(url)
data = response.json()
# Extract Rotten Tomatoes score from Ratings array
rt_score = None
metacritic_score = None
if "Ratings" in data:
for rating in data["Ratings"]:
if rating["Source"] == "Rotten Tomatoes":
rt_score = int(rating["Value"].replace("%", ""))
if rating["Source"] == "Metacritic":
metacritic_score = int(rating["Value"].split("/")[0])
# Parse box office (remove $ and commas)
box_office = data.get("BoxOffice", "N/A")
if box_office != "N/A" and box_office:
box_office = int(box_office.replace("$", "").replace(",", ""))
else:
box_office = None
return {
"imdb_rating": float(data.get("imdbRating", 0)) if data.get("imdbRating", "N/A") != "N/A" else None,
"rt_score": rt_score,
"metacritic_score": metacritic_score,
"box_office": box_office,
}
What does this function do?
get_omdb_data takes an IMDb ID like tt0468569 and sends a request to the OMDb API. The API responds with a JSON object containing all available metadata for that film.
The tricky part is that OMDb nests Rotten Tomatoes and Metacritic scores inside a Ratings array. Each entry has a Source and Value field. The function loops through this array to find and parse each score.
Box office data comes as a string like $533,316,061. The function strips the dollar sign and commas to convert it into a clean integer. Older films may not have box office data, so the function handles that gracefully by returning None.
- Save batman_classifier.py.
You won't see a change yet. The next function uses get_omdb_data to loop through all 11 films.
- Below get_omdb_data, add the collect_film_data function by pasting this code:
def collect_film_data():
"""Collect OMDb data for all Batman films."""
records = []
for film in BATMAN_FILMS:
print(f"Fetching OMDb data for: {film['title']} ({film['year']})")
omdb_data = get_omdb_data(film["imdb_id"])
record = {
"title": film["title"],
"year": film["year"],
"actor": film["actor"],
"imdb_id": film["imdb_id"],
"tmdb_id": film["tmdb_id"],
**omdb_data,
}
records.append(record)
return pd.DataFrame(records)
What does this function do?
collect_film_data loops through the BATMAN_FILMS list and calls get_omdb_data for each one. It merges the film's metadata (title, year, actor) with the API response data into a single dictionary per film.
The **omdb_data syntax unpacks the dictionary returned by get_omdb_data into the record. At the end, all records are converted into a pandas DataFrame for easy analysis.
- Save batman_classifier.py.
Seeing a red underline on any imports?
Make sure your virtual environment is active. VS Code sometimes picks the wrong Python interpreter.
Press Cmd+Shift+P (macOS) or Ctrl+Shift+P (Windows), type Python: Select Interpreter, and choose the interpreter inside your virtual environment folder.
help me fix import errors in VS Code
Run the data collection
With both functions defined, you need a main block that ties everything together. This block calls collect_film_data() and prints the resulting DataFrame so you can verify the data looks correct.
- At the bottom of batman_classifier.py, add the main execution block by pasting this code:
# ============================================================
# MAIN
# ============================================================
if __name__ == "__main__":
print("=" * 60)
print("BATMAN PERFORMANCE CLASSIFIER")
print("Who is the Best Batman? Let the data decide.")
print("=" * 60)
# Step 1: Collect film data
print("\n[1/5] Collecting film data from OMDb...")
df = collect_film_data()
print(f"\nCollected data for {len(df)} films:")
print(df[["title", "year", "actor", "imdb_rating", "rt_score"]].to_string(index=False))
What does this code do?
The if __name__ == "__main__": guard ensures this code only runs when you execute the file directly, not when it's imported as a module.
It prints a header, calls collect_film_data() to fetch data for all 11 films, then prints a table showing the title, year, actor, IMDb rating, and Rotten Tomatoes score for each film.
- Save batman_classifier.py.
- Run the script in your terminal by running this command:
python batman_classifier.py
You should see the script print "Fetching OMDb data for:" for each film, followed by a table showing all 11 Batman films with their ratings.
What should I see?
The output starts with the BATMAN PERFORMANCE CLASSIFIER header, then logs each film as it fetches data. At the end, you'll see a formatted table with columns for title, year, actor, imdb_rating, and rt_score.
Some older films may show None for certain fields. That's expected. The 1966 Batman film often lacks box office data in OMDb.
Seeing an error about your API key?
If you see a response containing an error about an invalid API key, double-check that you replaced your-omdb-api-key-here with your actual OMDb key. Make sure there are no extra spaces inside the quotes.
The OMDb free tier allows 1,000 requests per day. This script only makes 11 requests, so you won't hit the limit.
help me debug my OMDb API connection
✔️ Awesome, I've got everything!
Great. Double check you've saved batman_classifier.py and your terminal shows data for all 11 films.
ⓧ I'd like to double check the full code
Here's the complete batman_classifier.py file so far:
import requests
import pandas as pd
from textblob import TextBlob
import matplotlib.pyplot as plt
import numpy as np
# ============================================================
# CONFIGURATION
# ============================================================
OMDB_API_KEY = "your-omdb-api-key-here"
TMDB_API_KEY = "your-tmdb-api-key-here"
# Every live-action Batman film mapped to actor and TMDB ID
BATMAN_FILMS = [
{"title": "Batman", "year": 1966, "imdb_id": "tt0060153", "tmdb_id": 2661, "actor": "Adam West"},
{"title": "Batman", "year": 1989, "imdb_id": "tt0096895", "tmdb_id": 268, "actor": "Michael Keaton"},
{"title": "Batman Returns", "year": 1992, "imdb_id": "tt0103776", "tmdb_id": 364, "actor": "Michael Keaton"},
{"title": "Batman Forever", "year": 1995, "imdb_id": "tt0112462", "tmdb_id": 414, "actor": "Val Kilmer"},
{"title": "Batman & Robin", "year": 1997, "imdb_id": "tt0118688", "tmdb_id": 415, "actor": "George Clooney"},
{"title": "Batman Begins", "year": 2005, "imdb_id": "tt0372784", "tmdb_id": 272, "actor": "Christian Bale"},
{"title": "The Dark Knight", "year": 2008, "imdb_id": "tt0468569", "tmdb_id": 155, "actor": "Christian Bale"},
{"title": "The Dark Knight Rises", "year": 2012, "imdb_id": "tt1345836", "tmdb_id": 49026, "actor": "Christian Bale"},
{"title": "Batman v Superman: Dawn of Justice", "year": 2016, "imdb_id": "tt2975590", "tmdb_id": 209112, "actor": "Ben Affleck"},
{"title": "Justice League", "year": 2017, "imdb_id": "tt0974015", "tmdb_id": 141052, "actor": "Ben Affleck"},
{"title": "The Batman", "year": 2022, "imdb_id": "tt1877830", "tmdb_id": 414906, "actor": "Robert Pattinson"},
]
# ============================================================
# STEP 1: COLLECT FILM DATA FROM OMDb
# ============================================================
def get_omdb_data(imdb_id):
"""Query OMDb API for a single film by IMDb ID."""
url = f"http://www.omdbapi.com/?i={imdb_id}&apikey={OMDB_API_KEY}"
response = requests.get(url)
data = response.json()
# Extract Rotten Tomatoes score from Ratings array
rt_score = None
metacritic_score = None
if "Ratings" in data:
for rating in data["Ratings"]:
if rating["Source"] == "Rotten Tomatoes":
rt_score = int(rating["Value"].replace("%", ""))
if rating["Source"] == "Metacritic":
metacritic_score = int(rating["Value"].split("/")[0])
# Parse box office (remove $ and commas)
box_office = data.get("BoxOffice", "N/A")
if box_office != "N/A" and box_office:
box_office = int(box_office.replace("$", "").replace(",", ""))
else:
box_office = None
return {
"imdb_rating": float(data.get("imdbRating", 0)) if data.get("imdbRating", "N/A") != "N/A" else None,
"rt_score": rt_score,
"metacritic_score": metacritic_score,
"box_office": box_office,
}
def collect_film_data():
"""Collect OMDb data for all Batman films."""
records = []
for film in BATMAN_FILMS:
print(f"Fetching OMDb data for: {film['title']} ({film['year']})")
omdb_data = get_omdb_data(film["imdb_id"])
record = {
"title": film["title"],
"year": film["year"],
"actor": film["actor"],
"imdb_id": film["imdb_id"],
"tmdb_id": film["tmdb_id"],
**omdb_data,
}
records.append(record)
return pd.DataFrame(records)
# ============================================================
# MAIN
# ============================================================
if __name__ == "__main__":
print("=" * 60)
print("BATMAN PERFORMANCE CLASSIFIER")
print("Who is the Best Batman? Let the data decide.")
print("=" * 60)
# Step 1: Collect film data
print("\n[1/5] Collecting film data from OMDb...")
df = collect_film_data()
print(f"\nCollected data for {len(df)} films:")
print(df[["title", "year", "actor", "imdb_rating", "rt_score"]].to_string(index=False))
You now have real ratings data for all 11 Batman films sitting in a DataFrame. Next up, you'll pull audience reviews from TMDB so you can analyze what people actually said about each Batman performance.
Pull Audience Reviews from TMDB
You now have a DataFrame packed with quantitative ratings from OMDb. Numbers tell part of the story, but they miss something critical: what audiences actually said about each Batman performance.
In this step, you will pull audience review text from the TMDB API. These written reviews contain opinions, emotions, and judgments that a simple star rating cannot capture. Later, you will feed this text into a sentiment analysis pipeline to quantify how positively or negatively audiences felt about each Batman.
In this step, get ready to:
- Write a function that fetches audience reviews from the TMDB API for a single film.
- Collect reviews for all 11 Batman films and store them keyed by IMDb ID.
- Run the script and verify how many reviews were collected per film.
Write the TMDB review fetcher
The TMDB API has a dedicated reviews endpoint that returns audience-written text for any film. Each response includes a results array where every entry has a content field containing the full review text.
- In batman_classifier.py, find the end of the collect_film_data() function (after its closing return statement).
- Add the following section comment and function below it:
# ============================================================
# STEP 2: PULL AUDIENCE REVIEWS FROM TMDB
# ============================================================
def get_tmdb_reviews(tmdb_id):
"""Fetch audience reviews from TMDB for a single film."""
url = f"https://api.themoviedb.org/3/movie/{tmdb_id}/reviews"
headers = {"Authorization": f"Bearer {TMDB_API_KEY}"}
response = requests.get(url, headers=headers)
data = response.json()
reviews = []
if "results" in data:
for review in data["results"]:
reviews.append(review["content"])
return reviews
What does this code do?
This function queries the TMDB reviews endpoint for a single film using its TMDB ID. The URL follows the pattern /3/movie/{tmdb_id}/reviews.
TMDB uses Bearer token authentication. The API key goes in the Authorization header, not as a URL parameter. The response comes back with a results array, and you extract just the content field from each review object.
Why Bearer token instead of a query parameter?
TMDB's current authentication approach uses a Read Access Token passed as a Bearer token in the request header. This is more secure than appending credentials to the URL because headers are not logged in browser history or server access logs.
- Save the file. You will not run it yet because this function needs to be called by a collector function you will write next.
Collect reviews for all Batman films
A single function call fetches reviews for one film. You need a collector that iterates through the entire DataFrame and builds a dictionary of reviews keyed by each film's IMDb ID.
- Directly below the get_tmdb_reviews function, add this collector function:
def collect_reviews(df):
"""Collect TMDB reviews for all films in the DataFrame."""
all_reviews = {}
for _, row in df.iterrows():
print(f"Fetching reviews for: {row['title']} ({row['year']})")
reviews = get_tmdb_reviews(row["tmdb_id"])
all_reviews[row["imdb_id"]] = reviews
print(f" Found {len(reviews)} reviews")
return all_reviews
What does this code do?
This function loops through every row of your Batman films DataFrame. For each film, it calls get_tmdb_reviews using the film's TMDB ID and stores the returned list of review strings in a dictionary.
The dictionary is keyed by IMDb ID (not TMDB ID) so it aligns with your existing DataFrame. It also prints a count of reviews per film so you can see the collection progress in real time.
- Save batman_classifier.py. You will not see a change yet because the main block does not call this function yet.
Update the main block and run the script
With both functions in place, you need to call collect_reviews from the main block to actually fetch the reviews when you run the script.
- Scroll down to the if __name__ == "__main__": block at the bottom of your file.
- Find this line:
print(df[["title", "year", "actor", "imdb_rating", "rt_score"]].to_string(index=False))
- Add these lines directly below it:
# Step 2: Collect reviews
print("\n[2/5] Collecting audience reviews from TMDB...")
all_reviews = collect_reviews(df)
What does this code do?
This adds the review collection as step 2 of 5 in your pipeline. The collect_reviews(df) call passes your existing DataFrame so it knows which films to fetch reviews for. The result is stored in all_reviews, a dictionary you will use in the next step for sentiment analysis.
- Save batman_classifier.py.
- Run the script by running:
python batman_classifier.py
- You should see the OMDb data collection output from before, followed by a new section showing each film's title and the number of reviews found. Some films may have 5-20 reviews. Others (especially older films) may have fewer.
Seeing a 401 error or empty reviews?
A 401 status means your TMDB API key is not authorized. Make sure you are using your Read Access Token (the long alphanumeric string), not the shorter API Key. The Bearer token is the Read Access Token from your TMDB API settings page.
If you see 0 reviews for every film, double-check that TMDB_API_KEY at the top of your file contains the correct token with no extra spaces or quotes around it.
Help me debug my TMDB API connection
✔️ Awesome, I've got everything!
Great. Double check you have saved your file before moving on.
ⓧ I'd like to double check the full code
Here is the complete batman_classifier.py file as it should look at the end of this step:
import requests
import pandas as pd
from textblob import TextBlob
import matplotlib.pyplot as plt
import numpy as np
# ============================================================
# CONFIGURATION
# ============================================================
OMDB_API_KEY = "your-omdb-api-key-here"
TMDB_API_KEY = "your-tmdb-api-key-here"
# Every live-action Batman film mapped to actor and TMDB ID
BATMAN_FILMS = [
{"title": "Batman", "year": 1966, "imdb_id": "tt0060153", "tmdb_id": 2661, "actor": "Adam West"},
{"title": "Batman", "year": 1989, "imdb_id": "tt0096895", "tmdb_id": 268, "actor": "Michael Keaton"},
{"title": "Batman Returns", "year": 1992, "imdb_id": "tt0103776", "tmdb_id": 364, "actor": "Michael Keaton"},
{"title": "Batman Forever", "year": 1995, "imdb_id": "tt0112462", "tmdb_id": 414, "actor": "Val Kilmer"},
{"title": "Batman & Robin", "year": 1997, "imdb_id": "tt0118688", "tmdb_id": 415, "actor": "George Clooney"},
{"title": "Batman Begins", "year": 2005, "imdb_id": "tt0372784", "tmdb_id": 272, "actor": "Christian Bale"},
{"title": "The Dark Knight", "year": 2008, "imdb_id": "tt0468569", "tmdb_id": 155, "actor": "Christian Bale"},
{"title": "The Dark Knight Rises", "year": 2012, "imdb_id": "tt1345836", "tmdb_id": 49026, "actor": "Christian Bale"},
{"title": "Batman v Superman: Dawn of Justice", "year": 2016, "imdb_id": "tt2975590", "tmdb_id": 209112, "actor": "Ben Affleck"},
{"title": "Justice League", "year": 2017, "imdb_id": "tt0974015", "tmdb_id": 141052, "actor": "Ben Affleck"},
{"title": "The Batman", "year": 2022, "imdb_id": "tt1877830", "tmdb_id": 414906, "actor": "Robert Pattinson"},
]
# ============================================================
# STEP 1: COLLECT FILM DATA FROM OMDb
# ============================================================
def get_omdb_data(imdb_id):
"""Query OMDb API for a single film by IMDb ID."""
url = f"http://www.omdbapi.com/?i={imdb_id}&apikey={OMDB_API_KEY}"
response = requests.get(url)
data = response.json()
rt_score = None
metacritic_score = None
if "Ratings" in data:
for rating in data["Ratings"]:
if rating["Source"] == "Rotten Tomatoes":
rt_score = int(rating["Value"].replace("%", ""))
if rating["Source"] == "Metacritic":
metacritic_score = int(rating["Value"].split("/")[0])
box_office = data.get("BoxOffice", "N/A")
if box_office != "N/A" and box_office:
box_office = int(box_office.replace("$", "").replace(",", ""))
else:
box_office = None
return {
"imdb_rating": float(data.get("imdbRating", 0)) if data.get("imdbRating", "N/A") != "N/A" else None,
"rt_score": rt_score,
"metacritic_score": metacritic_score,
"box_office": box_office,
}
def collect_film_data():
"""Collect OMDb data for all Batman films."""
records = []
for film in BATMAN_FILMS:
print(f"Fetching OMDb data for: {film['title']} ({film['year']})")
omdb_data = get_omdb_data(film["imdb_id"])
record = {
"title": film["title"],
"year": film["year"],
"actor": film["actor"],
"imdb_id": film["imdb_id"],
"tmdb_id": film["tmdb_id"],
**omdb_data,
}
records.append(record)
return pd.DataFrame(records)
# ============================================================
# STEP 2: PULL AUDIENCE REVIEWS FROM TMDB
# ============================================================
def get_tmdb_reviews(tmdb_id):
"""Fetch audience reviews from TMDB for a single film."""
url = f"https://api.themoviedb.org/3/movie/{tmdb_id}/reviews"
headers = {"Authorization": f"Bearer {TMDB_API_KEY}"}
response = requests.get(url, headers=headers)
data = response.json()
reviews = []
if "results" in data:
for review in data["results"]:
reviews.append(review["content"])
return reviews
def collect_reviews(df):
"""Collect TMDB reviews for all films in the DataFrame."""
all_reviews = {}
for _, row in df.iterrows():
print(f"Fetching reviews for: {row['title']} ({row['year']})")
reviews = get_tmdb_reviews(row["tmdb_id"])
all_reviews[row["imdb_id"]] = reviews
print(f" Found {len(reviews)} reviews")
return all_reviews
# ============================================================
# MAIN
# ============================================================
if __name__ == "__main__":
print("=" * 60)
print("BATMAN PERFORMANCE CLASSIFIER")
print("Who is the Best Batman? Let the data decide.")
print("=" * 60)
# Step 1: Collect film data
print("\n[1/5] Collecting film data from OMDb...")
df = collect_film_data()
print(f"\nCollected data for {len(df)} films:")
print(df[["title", "year", "actor", "imdb_rating", "rt_score"]].to_string(index=False))
# Step 2: Collect reviews
print("\n[2/5] Collecting audience reviews from TMDB...")
all_reviews = collect_reviews(df)
You now have raw audience review text for every Batman film in your dataset. Next, you will feed these reviews through sentiment analysis to quantify whether audiences loved or hated each Batman performance.
Analyze Sentiment of Audience Reviews
You now have raw audience reviews for every Batman film sitting in your all_reviews dictionary. Numbers like IMDb ratings tell you what critics think, but what about how audiences actually feel?
In this step, you will use TextBlob to run sentiment analysis on every review. TextBlob scores each piece of text on a polarity scale from -1.0 (very negative) to 1.0 (very positive). By averaging those scores, you will get a single sentiment number for each film and each actor.
In this step, get ready to:
- Write a sentiment scoring function that uses TextBlob polarity.
- Aggregate sentiment scores by actor across all their Batman films.
- Print a per-actor sentiment breakdown to see which Batman audiences loved most.
Write the sentiment scoring function
Each review is a block of free-form text. You need a function that takes a list of reviews, scores each one for emotional polarity, and returns a single average score for the whole list.
- In batman_classifier.py, find the comment line that says # STEP 2: PULL AUDIENCE REVIEWS FROM TMDB and scroll past the collect_reviews function below it.
- After the collect_reviews function, add the following section header and function:
# ============================================================
# STEP 3: SENTIMENT ANALYSIS
# ============================================================
def analyze_sentiment(reviews):
"""Calculate average sentiment polarity for a list of review texts."""
if not reviews:
return 0.0
polarities = []
for review in reviews:
blob = TextBlob(review)
polarities.append(blob.sentiment.polarity)
return sum(polarities) / len(polarities)
What does this code do?
This function takes a list of review strings and turns them into a single number representing audience mood.
- if not reviews: return 0.0 handles films with no reviews by returning a neutral score.
- TextBlob(review).sentiment.polarity returns a float between -1.0 (very negative) and 1.0 (very positive) for each review.
- The function averages all polarity scores to produce a single sentiment value per film.
- Save batman_classifier.py.
Seeing a NameError for TextBlob?
Make sure the import from textblob import TextBlob is at the top of your file. It should already be there from Step 1.
If you see a ModuleNotFoundError instead, make sure your virtual environment is activated and that you ran pip install textblob==0.20.0 in Step 1.
help me fix a TextBlob import error in my Batman classifier script
Add sentiment scores to your DataFrame
The analyze_sentiment function scores a single film's reviews. Now you need a wrapper function that loops through every film in your DataFrame, looks up its reviews, scores them, and adds a new sentiment column.
- Directly below the analyze_sentiment function, add this function:
def add_sentiment_scores(df, all_reviews):
"""Add sentiment scores to the DataFrame."""
sentiments = []
for _, row in df.iterrows():
reviews = all_reviews.get(row["imdb_id"], [])
sentiment = analyze_sentiment(reviews)
sentiments.append(sentiment)
df["sentiment"] = sentiments
return df
What does this code do?
- all_reviews.get(row["imdb_id"], []) looks up the reviews for each film using its IMDb ID as the key. If no reviews exist, it falls back to an empty list.
- df["sentiment"] = sentiments attaches the computed scores as a new column in the DataFrame. Each row now has a sentiment value alongside its ratings and box office data.
- Save batman_classifier.py.
Seeing a KeyError on imdb_id?
Double-check that the column name in your DataFrame is imdb_id (with an underscore). This must match the key used in collect_film_data from Step 2.
help me fix a KeyError in my add_sentiment_scores function
Run sentiment analysis and print the actor breakdown
With both functions defined, you can now wire them into your main block. The final print statement will group sentiment scores by actor, giving you a clear picture of which Batman audiences responded to most positively.
- Scroll down to the if __name__ == "__main__": block at the bottom of your file.
- Find the last line, which should be:
all_reviews = collect_reviews(df)
- Add the following code directly below that line:
# Step 3: Sentiment analysis
print("\n[3/5] Analyzing review sentiment...")
df = add_sentiment_scores(df, all_reviews)
print("\nSentiment by actor:")
print(df.groupby("actor")["sentiment"].mean().sort_values(ascending=False).to_string())
What does this code do?
- add_sentiment_scores(df, all_reviews) calls your sentiment function for every film and adds the sentiment column to the DataFrame.
- df.groupby("actor")["sentiment"].mean() groups all films by actor, then averages their sentiment scores. An actor with three films gets a single averaged value.
- .sort_values(ascending=False) puts the most positively reviewed actor at the top.
- Save batman_classifier.py.
- Run your script by entering this command in your terminal:
python batman_classifier.py
- You should see the script fetch OMDb data, collect TMDB reviews, then print a "Sentiment by actor" table listing each Batman actor with a polarity score between -1.0 and 1.0.
Seeing all zeros for sentiment?
If every actor shows 0.0, your TMDB API key might not be returning reviews. Check that your TMDB_API_KEY variable holds your Read Access Token (the long one), not the shorter API key string.
Also confirm that the Step 2 output showed "Found X reviews" with numbers greater than zero for at least some films.
help me debug why all my sentiment scores are zero
Why TextBlob for sentiment analysis?
TextBlob uses a pre-trained pattern analyzer that works out of the box. One line of code gives you a polarity score for any text. For this project, that simplicity keeps the focus on the data pipeline rather than on training or tuning an NLP model.
More advanced tools like transformer-based models would provide higher accuracy, but they require GPU resources and large dependencies. TextBlob is accurate enough to reveal meaningful differences between audience reactions to different Batman films.
✔️ Awesome, I've got everything!
Great. Double check you have saved your file before moving on.
ⓧ I'd like to double check the full code
Here is the full batman_classifier.py up to this point:
import requests
import pandas as pd
from textblob import TextBlob
import matplotlib.pyplot as plt
import numpy as np
# ============================================================
# CONFIGURATION
# ============================================================
OMDB_API_KEY = "your-omdb-api-key-here"
TMDB_API_KEY = "your-tmdb-api-key-here"
# Every live-action Batman film mapped to actor and TMDB ID
BATMAN_FILMS = [
{"title": "Batman", "year": 1966, "imdb_id": "tt0060153", "tmdb_id": 2661, "actor": "Adam West"},
{"title": "Batman", "year": 1989, "imdb_id": "tt0096895", "tmdb_id": 268, "actor": "Michael Keaton"},
{"title": "Batman Returns", "year": 1992, "imdb_id": "tt0103776", "tmdb_id": 364, "actor": "Michael Keaton"},
{"title": "Batman Forever", "year": 1995, "imdb_id": "tt0112462", "tmdb_id": 414, "actor": "Val Kilmer"},
{"title": "Batman & Robin", "year": 1997, "imdb_id": "tt0118688", "tmdb_id": 415, "actor": "George Clooney"},
{"title": "Batman Begins", "year": 2005, "imdb_id": "tt0372784", "tmdb_id": 272, "actor": "Christian Bale"},
{"title": "The Dark Knight", "year": 2008, "imdb_id": "tt0468569", "tmdb_id": 155, "actor": "Christian Bale"},
{"title": "The Dark Knight Rises", "year": 2012, "imdb_id": "tt1345836", "tmdb_id": 49026, "actor": "Christian Bale"},
{"title": "Batman v Superman: Dawn of Justice", "year": 2016, "imdb_id": "tt2975590", "tmdb_id": 209112, "actor": "Ben Affleck"},
{"title": "Justice League", "year": 2017, "imdb_id": "tt0974015", "tmdb_id": 141052, "actor": "Ben Affleck"},
{"title": "The Batman", "year": 2022, "imdb_id": "tt1877830", "tmdb_id": 414906, "actor": "Robert Pattinson"},
]
# ============================================================
# STEP 1: COLLECT FILM DATA FROM OMDb
# ============================================================
def get_omdb_data(imdb_id):
"""Query OMDb API for a single film by IMDb ID."""
url = f"http://www.omdbapi.com/?i={imdb_id}&apikey={OMDB_API_KEY}"
response = requests.get(url)
data = response.json()
# Extract Rotten Tomatoes score from Ratings array
rt_score = None
metacritic_score = None
if "Ratings" in data:
for rating in data["Ratings"]:
if rating["Source"] == "Rotten Tomatoes":
rt_score = int(rating["Value"].replace("%", ""))
if rating["Source"] == "Metacritic":
metacritic_score = int(rating["Value"].split("/")[0])
# Parse box office (remove $ and commas)
box_office = data.get("BoxOffice", "N/A")
if box_office != "N/A" and box_office:
box_office = int(box_office.replace("$", "").replace(",", ""))
else:
box_office = None
return {
"imdb_rating": float(data.get("imdbRating", 0)) if data.get("imdbRating", "N/A") != "N/A" else None,
"rt_score": rt_score,
"metacritic_score": metacritic_score,
"box_office": box_office,
}
def collect_film_data():
"""Collect OMDb data for all Batman films."""
records = []
for film in BATMAN_FILMS:
print(f"Fetching OMDb data for: {film['title']} ({film['year']})")
omdb_data = get_omdb_data(film["imdb_id"])
record = {
"title": film["title"],
"year": film["year"],
"actor": film["actor"],
"imdb_id": film["imdb_id"],
"tmdb_id": film["tmdb_id"],
**omdb_data,
}
records.append(record)
return pd.DataFrame(records)
# ============================================================
# STEP 2: PULL AUDIENCE REVIEWS FROM TMDB
# ============================================================
def get_tmdb_reviews(tmdb_id):
"""Fetch audience reviews from TMDB for a single film."""
url = f"https://api.themoviedb.org/3/movie/{tmdb_id}/reviews"
headers = {"Authorization": f"Bearer {TMDB_API_KEY}"}
response = requests.get(url, headers=headers)
data = response.json()
reviews = []
if "results" in data:
for review in data["results"]:
reviews.append(review["content"])
return reviews
def collect_reviews(df):
"""Collect TMDB reviews for all films in the DataFrame."""
all_reviews = {}
for _, row in df.iterrows():
print(f"Fetching reviews for: {row['title']} ({row['year']})")
reviews = get_tmdb_reviews(row["tmdb_id"])
all_reviews[row["imdb_id"]] = reviews
print(f" Found {len(reviews)} reviews")
return all_reviews
# ============================================================
# STEP 3: SENTIMENT ANALYSIS
# ============================================================
def analyze_sentiment(reviews):
"""Calculate average sentiment polarity for a list of review texts."""
if not reviews:
return 0.0
polarities = []
for review in reviews:
blob = TextBlob(review)
polarities.append(blob.sentiment.polarity)
return sum(polarities) / len(polarities)
def add_sentiment_scores(df, all_reviews):
"""Add sentiment scores to the DataFrame."""
sentiments = []
for _, row in df.iterrows():
reviews = all_reviews.get(row["imdb_id"], [])
sentiment = analyze_sentiment(reviews)
sentiments.append(sentiment)
df["sentiment"] = sentiments
return df
# ============================================================
# MAIN
# ============================================================
if __name__ == "__main__":
print("=" * 60)
print("BATMAN PERFORMANCE CLASSIFIER")
print("Who is the Best Batman? Let the data decide.")
print("=" * 60)
# Step 1: Collect film data
print("\n[1/5] Collecting film data from OMDb...")
df = collect_film_data()
print(f"\nCollected data for {len(df)} films:")
print(df[["title", "year", "actor", "imdb_rating", "rt_score"]].to_string(index=False))
# Step 2: Collect reviews
print("\n[2/5] Collecting audience reviews from TMDB...")
all_reviews = collect_reviews(df)
# Step 3: Sentiment analysis
print("\n[3/5] Analyzing review sentiment...")
df = add_sentiment_scores(df, all_reviews)
print("\nSentiment by actor:")
print(df.groupby("actor")["sentiment"].mean().sort_values(ascending=False).to_string())
You now have a sentiment dimension sitting alongside your numeric ratings. Next up, you will normalize all five scoring dimensions to the same 0-100 scale and combine them into a single weighted "Batman Score" that crowns the winner.
Build the Weighted Performance Model
You have ratings, box office numbers, and sentiment scores for every Batman film. The problem is that each metric lives on a completely different scale.
IMDb ratings range from 0 to 10. Rotten Tomatoes goes from 0 to 100. Box office revenue is in raw dollars. Sentiment polarity is -1 to 1. To compare them fairly, you need to bring every dimension to the same 0-100 scale, assign weights, and compute a single composite score per actor.
In this step, get ready to:
- Normalize all scoring dimensions to a 0-100 scale.
- Compute a weighted composite "Batman Score" per film.
- Rank actors by average score and print the final verdict.
Normalize scoring dimensions to a common scale
Min-max normalization takes the lowest value in a set and maps it to 0, and the highest value to 100. Everything else falls proportionally in between. This lets you compare IMDb ratings against box office dollars on equal footing.
- In batman_classifier.py, find the comment that says # STEP 3: SENTIMENT ANALYSIS and scroll down past the add_sentiment_scores function.
- Add the following section comment and normalization function below it:
# ============================================================
# STEP 4: WEIGHTED PERFORMANCE MODEL
# ============================================================
def normalize_column(series):
"""Min-max normalize a pandas Series to 0-100 scale."""
min_val = series.min()
max_val = series.max()
if max_val == min_val:
return pd.Series([50.0] * len(series), index=series.index)
return ((series - min_val) / (max_val - min_val)) * 100
What does this code do?
- series.min() and series.max() find the range of values in the column.
- The if max_val == min_val check handles edge cases where all values are identical. Without it, you'd divide by zero.
- The formula ((series - min_val) / (max_val - min_val)) * 100 scales every value to a 0-100 range.
- Save batman_classifier.py.
- You won't see a change yet. The next function uses normalize_column to scale each scoring dimension.
Define weights and compute the composite Batman Score
Not all data dimensions are equally important. A strong Rotten Tomatoes consensus carries more weight than raw box office numbers when judging performance as Batman. You'll assign percentage weights to each metric and multiply them together into a single composite score.
- Directly below your normalize_column function, add the build_scoring_model function:
def build_scoring_model(df):
"""Build weighted composite score for each film."""
df["imdb_norm"] = normalize_column(df["imdb_rating"].fillna(df["imdb_rating"].median()))
df["rt_norm"] = normalize_column(df["rt_score"].fillna(df["rt_score"].median()))
df["metacritic_norm"] = normalize_column(df["metacritic_score"].fillna(df["metacritic_score"].median()))
df["box_office_norm"] = normalize_column(df["box_office"].fillna(df["box_office"].median()))
# Sentiment is -1 to 1, scale to 0-100
df["sentiment_norm"] = ((df["sentiment"] + 1) / 2) * 100
# Weights: critical reception matters most
weights = {
"rt_norm": 0.30,
"sentiment_norm": 0.25,
"imdb_norm": 0.20,
"box_office_norm": 0.15,
"metacritic_norm": 0.10,
}
df["batman_score"] = sum(df[col] * weight for col, weight in weights.items())
return df
What does this code do?
- .fillna(df["column"].median()) handles missing data by substituting the median value. Older films like the 1966 Batman may not have box office data in OMDb.
- Sentiment uses a different formula because its scale is -1 to 1, not 0 to N. Adding 1 shifts it to 0-2, dividing by 2 gives 0-1, and multiplying by 100 gives 0-100.
- The weights dictionary assigns Rotten Tomatoes 30%, audience sentiment 25%, IMDb rating 20%, box office 15%, and Metacritic 10%. These add up to 1.0 (100%).
- The final sum() multiplies each normalized column by its weight and adds the results into a single batman_score column.
Why these specific weights?
Critical consensus (Rotten Tomatoes) gets the highest weight because it aggregates dozens of professional reviews into one score. Audience sentiment is second because it captures how real viewers felt. Box office gets a lower weight because ticket sales reflect marketing budgets and release timing, not just the quality of a Batman performance.
- Save batman_classifier.py.
- You won't see output yet. The next substep calls these functions from the main block and prints the rankings.
Rank actors and print the final verdict
Each film now has its own Batman Score. But actors like Christian Bale played Batman in three films. You need to average each actor's scores across all their films and rank them from highest to lowest.
- Directly below the build_scoring_model function, add the rank_actors function:
def rank_actors(df):
"""Group by actor and compute average Batman Score."""
actor_scores = df.groupby("actor").agg(
batman_score=("batman_score", "mean"),
num_films=("title", "count"),
films=("title", lambda x: ", ".join(x)),
).sort_values("batman_score", ascending=False).reset_index()
return actor_scores
What does this code do?
- groupby("actor") collects all films by the same actor into a single group.
- .agg() computes three things per group: the mean Batman Score, the number of films, and a comma-joined list of film titles.
- .sort_values("batman_score", ascending=False) puts the highest-scoring actor at the top.
Now you need to call both functions from the main block and print the final rankings.
- Scroll down to the if __name__ == "__main__": block at the bottom of your file.
- Find the last two lines of the sentiment section:
print("\nSentiment by actor:")
print(df.groupby("actor")["sentiment"].mean().sort_values(ascending=False).to_string())
- Add the following code directly below those lines to build the model and print rankings:
# Step 4: Build scoring model
print("\n[4/5] Building weighted performance model...")
df = build_scoring_model(df)
actor_scores = rank_actors(df)
print("\n" + "=" * 60)
print("FINAL BATMAN RANKINGS")
print("=" * 60)
for i, row in actor_scores.iterrows():
rank = actor_scores.index.get_loc(i) + 1
medal = ["1st", "2nd", "3rd"][rank - 1] if rank <= 3 else f"{rank}th"
print(f" {medal} {row['actor']:20s} | Score: {row['batman_score']:.1f} | Films: {row['num_films']}")
print("=" * 60)
winner = actor_scores.iloc[0]
print(f"\n THE DATA HAS SPOKEN: {winner['actor'].upper()} IS THE BEST BATMAN")
print(f" Composite Score: {winner['batman_score']:.1f}/100")
print(f" Films: {winner['films']}")
What does this code do?
- build_scoring_model(df) normalizes every column and computes the weighted batman_score for each film.
- rank_actors(df) averages scores by actor and sorts them from best to worst.
- The loop prints a ranked table with medal positions (1st, 2nd, 3rd) and declares the winner based on the highest composite score.
- Save batman_classifier.py.
- Run the script by running:
python batman_classifier.py
- You should see the script fetch data from both APIs, then print a ranked table of Batman actors with scores and a winner verdict at the bottom.
Seeing a KeyError or TypeError?
- A KeyError on a column name means a previous step didn't add that column to the DataFrame. Check that your add_sentiment_scores function assigns df["sentiment"] correctly.
- A TypeError in normalize_column usually means a column is entirely None. Confirm your OMDb API key is valid and returning real data.
- If the weights don't add up to 1.0, your scores will be skewed. Double check the weights dictionary values: 0.30 + 0.25 + 0.20 + 0.15 + 0.10 = 1.0.
Why is my Batman scoring model throwing an error?
✔️ 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
import requests
import pandas as pd
from textblob import TextBlob
import matplotlib.pyplot as plt
import numpy as np
# ============================================================
# CONFIGURATION
# ============================================================
OMDB_API_KEY = "your-omdb-api-key-here"
TMDB_API_KEY = "your-tmdb-api-key-here"
# Every live-action Batman film mapped to actor and TMDB ID
BATMAN_FILMS = [
{"title": "Batman", "year": 1966, "imdb_id": "tt0060153", "tmdb_id": 2661, "actor": "Adam West"},
{"title": "Batman", "year": 1989, "imdb_id": "tt0096895", "tmdb_id": 268, "actor": "Michael Keaton"},
{"title": "Batman Returns", "year": 1992, "imdb_id": "tt0103776", "tmdb_id": 364, "actor": "Michael Keaton"},
{"title": "Batman Forever", "year": 1995, "imdb_id": "tt0112462", "tmdb_id": 414, "actor": "Val Kilmer"},
{"title": "Batman & Robin", "year": 1997, "imdb_id": "tt0118688", "tmdb_id": 415, "actor": "George Clooney"},
{"title": "Batman Begins", "year": 2005, "imdb_id": "tt0372784", "tmdb_id": 272, "actor": "Christian Bale"},
{"title": "The Dark Knight", "year": 2008, "imdb_id": "tt0468569", "tmdb_id": 155, "actor": "Christian Bale"},
{"title": "The Dark Knight Rises", "year": 2012, "imdb_id": "tt1345836", "tmdb_id": 49026, "actor": "Christian Bale"},
{"title": "Batman v Superman: Dawn of Justice", "year": 2016, "imdb_id": "tt2975590", "tmdb_id": 209112, "actor": "Ben Affleck"},
{"title": "Justice League", "year": 2017, "imdb_id": "tt0974015", "tmdb_id": 141052, "actor": "Ben Affleck"},
{"title": "The Batman", "year": 2022, "imdb_id": "tt1877830", "tmdb_id": 414906, "actor": "Robert Pattinson"},
]
# ============================================================
# STEP 1: COLLECT FILM DATA FROM OMDb
# ============================================================
def get_omdb_data(imdb_id):
"""Query OMDb API for a single film by IMDb ID."""
url = f"http://www.omdbapi.com/?i={imdb_id}&apikey={OMDB_API_KEY}"
response = requests.get(url)
data = response.json()
rt_score = None
metacritic_score = None
if "Ratings" in data:
for rating in data["Ratings"]:
if rating["Source"] == "Rotten Tomatoes":
rt_score = int(rating["Value"].replace("%", ""))
if rating["Source"] == "Metacritic":
metacritic_score = int(rating["Value"].split("/")[0])
box_office = data.get("BoxOffice", "N/A")
if box_office != "N/A" and box_office:
box_office = int(box_office.replace("$", "").replace(",", ""))
else:
box_office = None
return {
"imdb_rating": float(data.get("imdbRating", 0)) if data.get("imdbRating", "N/A") != "N/A" else None,
"rt_score": rt_score,
"metacritic_score": metacritic_score,
"box_office": box_office,
}
def collect_film_data():
"""Collect OMDb data for all Batman films."""
records = []
for film in BATMAN_FILMS:
print(f"Fetching OMDb data for: {film['title']} ({film['year']})")
omdb_data = get_omdb_data(film["imdb_id"])
record = {
"title": film["title"],
"year": film["year"],
"actor": film["actor"],
"imdb_id": film["imdb_id"],
"tmdb_id": film["tmdb_id"],
**omdb_data,
}
records.append(record)
return pd.DataFrame(records)
# ============================================================
# STEP 2: PULL AUDIENCE REVIEWS FROM TMDB
# ============================================================
def get_tmdb_reviews(tmdb_id):
"""Fetch audience reviews from TMDB for a single film."""
url = f"https://api.themoviedb.org/3/movie/{tmdb_id}/reviews"
headers = {"Authorization": f"Bearer {TMDB_API_KEY}"}
response = requests.get(url, headers=headers)
data = response.json()
reviews = []
if "results" in data:
for review in data["results"]:
reviews.append(review["content"])
return reviews
def collect_reviews(df):
"""Collect TMDB reviews for all films in the DataFrame."""
all_reviews = {}
for _, row in df.iterrows():
print(f"Fetching reviews for: {row['title']} ({row['year']})")
reviews = get_tmdb_reviews(row["tmdb_id"])
all_reviews[row["imdb_id"]] = reviews
print(f" Found {len(reviews)} reviews")
return all_reviews
# ============================================================
# STEP 3: SENTIMENT ANALYSIS
# ============================================================
def analyze_sentiment(reviews):
"""Calculate average sentiment polarity for a list of review texts."""
if not reviews:
return 0.0
polarities = []
for review in reviews:
blob = TextBlob(review)
polarities.append(blob.sentiment.polarity)
return sum(polarities) / len(polarities)
def add_sentiment_scores(df, all_reviews):
"""Add sentiment scores to the DataFrame."""
sentiments = []
for _, row in df.iterrows():
reviews = all_reviews.get(row["imdb_id"], [])
sentiment = analyze_sentiment(reviews)
sentiments.append(sentiment)
df["sentiment"] = sentiments
return df
# ============================================================
# STEP 4: WEIGHTED PERFORMANCE MODEL
# ============================================================
def normalize_column(series):
"""Min-max normalize a pandas Series to 0-100 scale."""
min_val = series.min()
max_val = series.max()
if max_val == min_val:
return pd.Series([50.0] * len(series), index=series.index)
return ((series - min_val) / (max_val - min_val)) * 100
def build_scoring_model(df):
"""Build weighted composite score for each film."""
df["imdb_norm"] = normalize_column(df["imdb_rating"].fillna(df["imdb_rating"].median()))
df["rt_norm"] = normalize_column(df["rt_score"].fillna(df["rt_score"].median()))
df["metacritic_norm"] = normalize_column(df["metacritic_score"].fillna(df["metacritic_score"].median()))
df["box_office_norm"] = normalize_column(df["box_office"].fillna(df["box_office"].median()))
df["sentiment_norm"] = ((df["sentiment"] + 1) / 2) * 100
weights = {
"rt_norm": 0.30,
"sentiment_norm": 0.25,
"imdb_norm": 0.20,
"box_office_norm": 0.15,
"metacritic_norm": 0.10,
}
df["batman_score"] = sum(df[col] * weight for col, weight in weights.items())
return df
def rank_actors(df):
"""Group by actor and compute average Batman Score."""
actor_scores = df.groupby("actor").agg(
batman_score=("batman_score", "mean"),
num_films=("title", "count"),
films=("title", lambda x: ", ".join(x)),
).sort_values("batman_score", ascending=False).reset_index()
return actor_scores
# ============================================================
# MAIN
# ============================================================
if __name__ == "__main__":
print("=" * 60)
print("BATMAN PERFORMANCE CLASSIFIER")
print("Who is the Best Batman? Let the data decide.")
print("=" * 60)
# Step 1: Collect film data
print("\n[1/5] Collecting film data from OMDb...")
df = collect_film_data()
print(f"\nCollected data for {len(df)} films:")
print(df[["title", "year", "actor", "imdb_rating", "rt_score"]].to_string(index=False))
# Step 2: Collect reviews
print("\n[2/5] Collecting audience reviews from TMDB...")
all_reviews = collect_reviews(df)
# Step 3: Sentiment analysis
print("\n[3/5] Analyzing review sentiment...")
df = add_sentiment_scores(df, all_reviews)
print("\nSentiment by actor:")
print(df.groupby("actor")["sentiment"].mean().sort_values(ascending=False).to_string())
# Step 4: Build scoring model
print("\n[4/5] Building weighted performance model...")
df = build_scoring_model(df)
actor_scores = rank_actors(df)
print("\n" + "=" * 60)
print("FINAL BATMAN RANKINGS")
print("=" * 60)
for i, row in actor_scores.iterrows():
rank = actor_scores.index.get_loc(i) + 1
medal = ["1st", "2nd", "3rd"][rank - 1] if rank <= 3 else f"{rank}th"
print(f" {medal} {row['actor']:20s} | Score: {row['batman_score']:.1f} | Films: {row['num_films']}")
print("=" * 60)
winner = actor_scores.iloc[0]
print(f"\n THE DATA HAS SPOKEN: {winner['actor'].upper()} IS THE BEST BATMAN")
print(f" Composite Score: {winner['batman_score']:.1f}/100")
print(f" Films: {winner['films']}")
The data has spoken. You now have a ranked scorecard backed by real numbers. Next up, you'll turn this ranking into a polished visualization that tells the story at a glance.
Visualize the Results
Your scoring model is complete and the ranked table is printing to your terminal. Numbers in a console are useful for debugging, but a chart tells the story instantly.
In this step, you'll use matplotlib to turn those composite scores into a color-coded horizontal bar chart. The chart will crown the best Batman at a glance and save as an image you can share.
In this step, get ready to:
- Create a horizontal bar chart ranking every Batman actor by composite score.
- Add color-coding and score annotations to the chart.
- Save the final visualization as batman_rankings.png and run the full pipeline end-to-end.
Write the visualization function
A horizontal bar chart works well here because actor names read naturally on the y-axis and the bar lengths make score differences obvious. You'll also color-code the bars from red (lowest) to green (highest) so the winner pops visually.
- In batman_classifier.py, find the comment section # MAIN near the bottom of your file.
- Add the following function ABOVE that # MAIN section:
# ============================================================
# STEP 5: VISUALIZATION
# ============================================================
def create_ranking_chart(actor_scores):
"""Create a horizontal bar chart ranking Batman actors."""
fig, ax = plt.subplots(figsize=(12, 7))
colors = plt.cm.RdYlGn(actor_scores["batman_score"] / 100)
bars = ax.barh(
actor_scores["actor"],
actor_scores["batman_score"],
color=colors,
edgecolor="black",
linewidth=0.5,
)
# Add score labels
for bar, score in zip(bars, actor_scores["batman_score"]):
ax.text(
bar.get_width() + 1,
bar.get_y() + bar.get_height() / 2,
f"{score:.1f}",
va="center",
fontweight="bold",
fontsize=11,
)
ax.set_xlabel("Composite Batman Score (0-100)", fontsize=12)
ax.set_title("Who Is the Best Batman? The Data Decides.", fontsize=14, fontweight="bold")
ax.set_xlim(0, 105)
ax.invert_yaxis()
plt.tight_layout()
plt.savefig("batman_rankings.png", dpi=150, bbox_inches="tight")
plt.show()
print("\nChart saved as batman_rankings.png")
What does this code do?
- plt.subplots(figsize=(12, 7)) creates a 12x7 inch figure. This gives enough horizontal space for the bars and labels.
- plt.cm.RdYlGn is a colormap that goes from red through yellow to green. Dividing each score by 100 maps it to the right position on that gradient.
- ax.barh() draws horizontal bars. The loop below it adds each actor's numeric score as a text label to the right of their bar.
- ax.invert_yaxis() flips the chart so the highest-scoring actor appears at the top.
- plt.savefig() saves the chart as a PNG image at 150 dpi before plt.show() displays it in a window.
Call the chart from your main block
The function is defined but nothing calls it yet. You need to add a step 5 call at the very end of the main block.
- Scroll to the bottom of your if __name__ == "__main__": block.
- Find this line (the last line currently in main):
print(f" Films: {winner['films']}")
- Add the following lines below it:
# Step 5: Visualize
print("\n[5/5] Creating visualization...")
create_ranking_chart(actor_scores)
What does this code do?
This adds the final stage to your pipeline. After printing the text-based rankings, the script now generates and saves the visual chart as the last action.
Run the complete pipeline
Your script is now complete. Time to run the full pipeline end-to-end and see the chart.
- Save batman_classifier.py.
- Run the full script by executing this command in your terminal:
python batman_classifier.py
- A matplotlib window should pop up showing your horizontal bar chart with all Batman actors ranked from top to bottom.
- Close the chart window when you are done viewing it.
- Confirm that batman_rankings.png was saved in your project folder by running:
ls
- You should see batman_rankings.png listed alongside batman_classifier.py in the output.
What should I see?
Your terminal prints the full pipeline output. First it fetches OMDb data for all 11 films, then TMDB reviews, then sentiment scores, then the ranked table with 1st/2nd/3rd place labels.
At the end you'll see a "THE DATA HAS SPOKEN" line declaring the winner. The chart window shows a horizontal bar chart with green bars for high scorers and red bars for low scorers. Each bar has its numeric score printed to the right.
Chart window not appearing?
- If you are running the script over SSH or in a headless environment, matplotlib cannot display a window. The chart still saves as batman_rankings.png. Open that file directly to see your results.
- If the script hangs without showing anything, check that matplotlib is installed correctly by running pip show matplotlib in your terminal.
- If you see API errors earlier in the output, double check that your API keys are correctly pasted into the configuration section at the top of the file.
Still stuck? Help me debug why my Batman rankings chart is not displaying.
✔️ Awesome, I've got everything!
Great. Double check you have saved your file before moving on.
ⓧ I'd like to double check the full code
import requests
import pandas as pd
from textblob import TextBlob
import matplotlib.pyplot as plt
import numpy as np
# ============================================================
# CONFIGURATION
# ============================================================
OMDB_API_KEY = "your-omdb-api-key-here"
TMDB_API_KEY = "your-tmdb-api-key-here"
# Every live-action Batman film mapped to actor and TMDB ID
BATMAN_FILMS = [
{"title": "Batman", "year": 1966, "imdb_id": "tt0060153", "tmdb_id": 2661, "actor": "Adam West"},
{"title": "Batman", "year": 1989, "imdb_id": "tt0096895", "tmdb_id": 268, "actor": "Michael Keaton"},
{"title": "Batman Returns", "year": 1992, "imdb_id": "tt0103776", "tmdb_id": 364, "actor": "Michael Keaton"},
{"title": "Batman Forever", "year": 1995, "imdb_id": "tt0112462", "tmdb_id": 414, "actor": "Val Kilmer"},
{"title": "Batman & Robin", "year": 1997, "imdb_id": "tt0118688", "tmdb_id": 415, "actor": "George Clooney"},
{"title": "Batman Begins", "year": 2005, "imdb_id": "tt0372784", "tmdb_id": 272, "actor": "Christian Bale"},
{"title": "The Dark Knight", "year": 2008, "imdb_id": "tt0468569", "tmdb_id": 155, "actor": "Christian Bale"},
{"title": "The Dark Knight Rises", "year": 2012, "imdb_id": "tt1345836", "tmdb_id": 49026, "actor": "Christian Bale"},
{"title": "Batman v Superman: Dawn of Justice", "year": 2016, "imdb_id": "tt2975590", "tmdb_id": 209112, "actor": "Ben Affleck"},
{"title": "Justice League", "year": 2017, "imdb_id": "tt0974015", "tmdb_id": 141052, "actor": "Ben Affleck"},
{"title": "The Batman", "year": 2022, "imdb_id": "tt1877830", "tmdb_id": 414906, "actor": "Robert Pattinson"},
]
# ============================================================
# STEP 1: COLLECT FILM DATA FROM OMDb
# ============================================================
def get_omdb_data(imdb_id):
"""Query OMDb API for a single film by IMDb ID."""
url = f"http://www.omdbapi.com/?i={imdb_id}&apikey={OMDB_API_KEY}"
response = requests.get(url)
data = response.json()
# Extract Rotten Tomatoes score from Ratings array
rt_score = None
metacritic_score = None
if "Ratings" in data:
for rating in data["Ratings"]:
if rating["Source"] == "Rotten Tomatoes":
rt_score = int(rating["Value"].replace("%", ""))
if rating["Source"] == "Metacritic":
metacritic_score = int(rating["Value"].split("/")[0])
# Parse box office (remove $ and commas)
box_office = data.get("BoxOffice", "N/A")
if box_office != "N/A" and box_office:
box_office = int(box_office.replace("$", "").replace(",", ""))
else:
box_office = None
return {
"imdb_rating": float(data.get("imdbRating", 0)) if data.get("imdbRating", "N/A") != "N/A" else None,
"rt_score": rt_score,
"metacritic_score": metacritic_score,
"box_office": box_office,
}
def collect_film_data():
"""Collect OMDb data for all Batman films."""
records = []
for film in BATMAN_FILMS:
print(f"Fetching OMDb data for: {film['title']} ({film['year']})")
omdb_data = get_omdb_data(film["imdb_id"])
record = {
"title": film["title"],
"year": film["year"],
"actor": film["actor"],
"imdb_id": film["imdb_id"],
"tmdb_id": film["tmdb_id"],
**omdb_data,
}
records.append(record)
return pd.DataFrame(records)
# ============================================================
# STEP 2: PULL AUDIENCE REVIEWS FROM TMDB
# ============================================================
def get_tmdb_reviews(tmdb_id):
"""Fetch audience reviews from TMDB for a single film."""
url = f"https://api.themoviedb.org/3/movie/{tmdb_id}/reviews"
headers = {"Authorization": f"Bearer {TMDB_API_KEY}"}
response = requests.get(url, headers=headers)
data = response.json()
reviews = []
if "results" in data:
for review in data["results"]:
reviews.append(review["content"])
return reviews
def collect_reviews(df):
"""Collect TMDB reviews for all films in the DataFrame."""
all_reviews = {}
for _, row in df.iterrows():
print(f"Fetching reviews for: {row['title']} ({row['year']})")
reviews = get_tmdb_reviews(row["tmdb_id"])
all_reviews[row["imdb_id"]] = reviews
print(f" Found {len(reviews)} reviews")
return all_reviews
# ============================================================
# STEP 3: SENTIMENT ANALYSIS
# ============================================================
def analyze_sentiment(reviews):
"""Calculate average sentiment polarity for a list of review texts."""
if not reviews:
return 0.0
polarities = []
for review in reviews:
blob = TextBlob(review)
polarities.append(blob.sentiment.polarity)
return sum(polarities) / len(polarities)
def add_sentiment_scores(df, all_reviews):
"""Add sentiment scores to the DataFrame."""
sentiments = []
for _, row in df.iterrows():
reviews = all_reviews.get(row["imdb_id"], [])
sentiment = analyze_sentiment(reviews)
sentiments.append(sentiment)
df["sentiment"] = sentiments
return df
# ============================================================
# STEP 4: WEIGHTED PERFORMANCE MODEL
# ============================================================
def normalize_column(series):
"""Min-max normalize a pandas Series to 0-100 scale."""
min_val = series.min()
max_val = series.max()
if max_val == min_val:
return pd.Series([50.0] * len(series), index=series.index)
return ((series - min_val) / (max_val - min_val)) * 100
def build_scoring_model(df):
"""Build weighted composite score for each film."""
df["imdb_norm"] = normalize_column(df["imdb_rating"].fillna(df["imdb_rating"].median()))
df["rt_norm"] = normalize_column(df["rt_score"].fillna(df["rt_score"].median()))
df["metacritic_norm"] = normalize_column(df["metacritic_score"].fillna(df["metacritic_score"].median()))
df["box_office_norm"] = normalize_column(df["box_office"].fillna(df["box_office"].median()))
df["sentiment_norm"] = ((df["sentiment"] + 1) / 2) * 100
weights = {
"rt_norm": 0.30,
"sentiment_norm": 0.25,
"imdb_norm": 0.20,
"box_office_norm": 0.15,
"metacritic_norm": 0.10,
}
df["batman_score"] = sum(df[col] * weight for col, weight in weights.items())
return df
def rank_actors(df):
"""Group by actor and compute average Batman Score."""
actor_scores = df.groupby("actor").agg(
batman_score=("batman_score", "mean"),
num_films=("title", "count"),
films=("title", lambda x: ", ".join(x)),
).sort_values("batman_score", ascending=False).reset_index()
return actor_scores
# ============================================================
# STEP 5: VISUALIZATION
# ============================================================
def create_ranking_chart(actor_scores):
"""Create a horizontal bar chart ranking Batman actors."""
fig, ax = plt.subplots(figsize=(12, 7))
colors = plt.cm.RdYlGn(actor_scores["batman_score"] / 100)
bars = ax.barh(
actor_scores["actor"],
actor_scores["batman_score"],
color=colors,
edgecolor="black",
linewidth=0.5,
)
# Add score labels
for bar, score in zip(bars, actor_scores["batman_score"]):
ax.text(
bar.get_width() + 1,
bar.get_y() + bar.get_height() / 2,
f"{score:.1f}",
va="center",
fontweight="bold",
fontsize=11,
)
ax.set_xlabel("Composite Batman Score (0-100)", fontsize=12)
ax.set_title("Who Is the Best Batman? The Data Decides.", fontsize=14, fontweight="bold")
ax.set_xlim(0, 105)
ax.invert_yaxis()
plt.tight_layout()
plt.savefig("batman_rankings.png", dpi=150, bbox_inches="tight")
plt.show()
print("\nChart saved as batman_rankings.png")
# ============================================================
# MAIN
# ============================================================
if __name__ == "__main__":
print("=" * 60)
print("BATMAN PERFORMANCE CLASSIFIER")
print("Who is the Best Batman? Let the data decide.")
print("=" * 60)
# Step 1: Collect film data
print("\n[1/5] Collecting film data from OMDb...")
df = collect_film_data()
print(f"\nCollected data for {len(df)} films:")
print(df[["title", "year", "actor", "imdb_rating", "rt_score"]].to_string(index=False))
# Step 2: Collect reviews
print("\n[2/5] Collecting audience reviews from TMDB...")
all_reviews = collect_reviews(df)
# Step 3: Sentiment analysis
print("\n[3/5] Analyzing review sentiment...")
df = add_sentiment_scores(df, all_reviews)
print("\nSentiment by actor:")
print(df.groupby("actor")["sentiment"].mean().sort_values(ascending=False).to_string())
# Step 4: Build scoring model
print("\n[4/5] Building weighted performance model...")
df = build_scoring_model(df)
actor_scores = rank_actors(df)
print("\n" + "=" * 60)
print("FINAL BATMAN RANKINGS")
print("=" * 60)
for i, row in actor_scores.iterrows():
rank = actor_scores.index.get_loc(i) + 1
medal = ["1st", "2nd", "3rd"][rank - 1] if rank <= 3 else f"{rank}th"
print(f" {medal} {row['actor']:20s} | Score: {row['batman_score']:.1f} | Films: {row['num_films']}")
print("=" * 60)
winner = actor_scores.iloc[0]
print(f"\n THE DATA HAS SPOKEN: {winner['actor'].upper()} IS THE BEST BATMAN")
print(f" Composite Score: {winner['batman_score']:.1f}/100")
print(f" Films: {winner['films']}")
# Step 5: Visualize
print("\n[5/5] Creating visualization...")
create_ranking_chart(actor_scores)
Secret mission
Batman Eras Timeline
Your ranking chart shows who the best Batman is. But it doesn't show when. Add a timeline visualization that plots each film's score against its release year, revealing whether audiences have gotten kinder or harsher over time.
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely on your local machine using free API tiers. There are no ongoing costs or cloud resources to worry about.
Decide whether to keep your project files for further experimentation, or delete them to free up space.
Resources you used:
- Your project folder (Python scripts, virtual environment, output images).
- TextBlob corpora downloaded to your system.
- OMDb API account (free tier).
- TMDB API account (free tier).
Keep everything running
No action needed. Choose this if you're still actively experimenting with different weights, adding new Batman films, or tweaking your visualizations.
Your OMDb and TMDB API accounts remain active and free for personal use. Your project files stay on your Desktop ready for the next session.
Pause - I'll come back to this later
Since this is a script-based project with no running processes, there's nothing to pause. Your API keys remain valid whenever you return.
- Deactivate your virtual environment (if active) by running:
- Your project folder and API keys will be waiting when you're ready to pick this up again.
deactivate
Delete - I don't want to use this again
Remove all project files and downloaded data to start fresh.
- Delete your project folder by running these commands in your terminal:
macOS
cd ~/Desktop
rm -rf batman-classifier
Windows
cd ~\Desktop
rmdir /s /q batman-classifier
- Delete the TextBlob corpora (NLTK data) that was downloaded to your home directory:
macOS
rm -rf ~/nltk_data
Windows
rmdir /s /q %USERPROFILE%\nltk_data
Your OMDb and TMDB accounts are free and can remain active without any cost. If you want to revoke API access, you can delete your API keys from each service's settings page.
That's a Wrap!
That's a Wrap!
Nice work! You just used data science to settle one of pop culture's longest-running debates. Your Batman Performance Classifier pulls real ratings, analyzes audience sentiment, and delivers a verdict backed by numbers.
You've learned how to:
- Built an API data pipeline that collects ratings, box office data, and audience reviews from the OMDb API and TMDB API for every live-action Batman film.
- Performed NLP sentiment analysis on audience reviews using TextBlob to quantify how audiences feel about each Batman actor.
- Engineered a weighted scoring model combining five data dimensions with min-max normalization, and visualized the final rankings using matplotlib.
- Secret Mission: Added a Batman Eras timeline visualization showing how film quality has evolved across decades with a polynomial trend line.
Ready to quiz yourself?