Build a Movie Recommender
Build a local movie recommender with TF-IDF, cosine similarity, and Streamlit.
Introduction
30 Second Summary
Choosing a movie can take longer than watching its trailer. Broad categories help narrow the options, but two films in the same genre can tell completely different stories.
In this project, you will build a local movie recommender with Python and Streamlit. You will use content-based filtering to turn movie genres and descriptions into ranked suggestions that appear in your browser.
What You'll Build
You will select a fictional movie such as Orbit's Edge. MovieMatch AI responds with five related titles, story descriptions, and similarity percentages.
By the end of this project, you'll have:
- A browser-based movie catalog where you can inspect fictional titles and genres before requesting recommendations.
- An explainable recommendation pipeline that uses TF-IDF to convert movie text into features.
- A ranked list of five recommendations where cosine similarity scores reveal how closely each movie matches your selection.
- Secret Mission: Add an optional genre filter that preserves the similarity ranking among the remaining movies.
Are there any prerequisites?
You need a Windows computer with Python 3.11 or newer, Visual Studio Code, and a web browser.
Basic familiarity with files and folders is enough. The Python syntax is introduced as you use it.
Before We Start
First, lock in who MovieMatch AI is for and what you want its recommendations to help them discover. This gives your Python and AI work a clear purpose before the hands-on build begins.
Prepare the Windows Project
You have chosen who MovieMatch AI should help. Now you need a dependable Windows workspace where its recommendation tools can run.
The pinned libraries need Python 3.11 or newer. A virtual environment keeps these project packages separate from your other Python projects.
In this step, get ready to:
- Create the requirements.txt file inside movie-match-ai using Visual Studio Code.
- Build an activated Python environment inside .venv.
- Launch the local example from Streamlit.
Create the project files
The requirements.txt file records the exact package versions used by MovieMatch AI. This gives every installation the same starting point.
- Press the Windows key to open Windows search.
- Type Visual Studio Code into the search field.
- Press Enter to open Visual Studio Code.
- Choose the folder-opening option from the Visual Studio Code welcome screen.
- Select your Desktop in the folder picker.
- Choose the new-folder control in the folder picker.
- Enter movie-match-ai as the folder name.
- Select movie-match-ai as the folder to open.
You'll see movie-match-ai at the top of the file sidebar. That name confirms Visual Studio Code is working inside the correct folder.
- Choose the new-file control in the file sidebar.
- Enter requirements.txt as the file name.
- Add the pinned dependencies by copying this content into the file:
pandas==3.0.5
scikit-learn==1.9.0
streamlit==1.62.0
What does this file control?
- The pandas package at version 3.0.5 stores the movie catalog in a table.
- The scikit-learn package at version 1.9.0 provides the text-vector and similarity tools used later.
- The Streamlit package at version 1.62.0 serves the browser interface from your computer.
- Press Ctrl+S to save requirements.txt.
- Select requirements.txt in the file sidebar to verify its contents.
You'll see three saved lines. Each line contains one package name followed by its pinned version.
File missing or in the wrong folder?
- Confirm the file sidebar shows movie-match-ai at the top.
- Move requirements.txt into that folder if it appears somewhere else.
Use this prompt if the file still does not appear. Help me create requirements.txt inside my movie-match-ai folder in Visual Studio Code on Windows.
✔️ Awesome, I've got everything!
Great. Save requirements.txt before moving on.
ⓧ I'd like to double check the full code
pandas==3.0.5
scikit-learn==1.9.0
streamlit==1.62.0
What should match?
Your file should contain these three lines in this order. Matching the versions prevents package differences from changing the project later.
Verify Python and create the environment
Package isolation only helps when the underlying Python version supports every dependency. The version check confirms that foundation before anything is installed.
Why use a virtual environment?
A virtual environment gives movie-match-ai its own package area. Its pinned versions stay separate from packages used by other projects.
This isolation makes errors easier to reproduce. It also keeps project upgrades contained.
- Open the integrated terminal from the terminal menu in Visual Studio Code.
- Check your installed Python version by running this command:
python --version
What does this command check?
This command prints the Python version used by the current terminal. The pinned pandas and scikit-learn packages require Python 3.11 or newer.
You'll see Python version 3.11 or higher before the terminal prompt returns.
Python version below 3.11?
A lower version cannot support the pinned data and machine-learning packages. Update Python with the official Windows installer before continuing.
Use this prompt if the terminal still finds the older installation. Help me make Visual Studio Code use Python 3.11 or newer on Windows.
- Create the isolated .venv environment by running this command:
python -m venv .venv
What does this command create?
Python creates a .venv folder inside movie-match-ai. That folder holds an isolated Python interpreter plus the packages you install next.
- Check the file sidebar for the new .venv folder.
You'll see .venv listed beside requirements.txt. This confirms the environment exists inside your project.
The .venv folder did not appear?
Confirm the terminal is inside movie-match-ai. Also confirm the Python version check succeeded before you retry the environment command.
Use this prompt for help with the environment. Help me diagnose why Python did not create .venv in my movie-match-ai folder.
The activation command depends on whether the Visual Studio Code terminal uses PowerShell or Command Prompt. Choose the tab matching the shell name shown in your terminal.
PowerShell
- Activate .venv in PowerShell by running this command:
.venv\Scripts\Activate.ps1
What does activation change?
Activation points the current PowerShell session at the Python interpreter inside .venv. Package commands now target this project environment.
- Check the beginning of the terminal prompt for the environment name.
You'll see .venv near the start of the prompt. That label confirms the environment is active.
PowerShell blocked activation?
PowerShell can prevent local activation scripts from running. Use the Command Prompt tab as the fallback path for this project.
Use this prompt if you want to understand the block first. Help me understand why PowerShell blocked my .venv activation script on Windows.
Command Prompt
- Activate .venv in Command Prompt by running this command:
.venv\Scripts\activate.bat
What does activation change?
Activation points the current Command Prompt session at the Python interpreter inside .venv. Package commands now target this project environment.
- Check the beginning of the terminal prompt for the environment name.
You'll see .venv near the start of the prompt. That label confirms the environment is active.
The environment name did not appear?
Confirm the terminal is inside movie-match-ai. The activation path only works from the folder that contains .venv.
Use this prompt to inspect the terminal location. Help me activate .venv from my movie-match-ai folder in Command Prompt.
Install dependencies and test Streamlit
The active environment is ready to receive the exact packages listed in requirements.txt. The pip command reads that file and installs each pin into .venv.
- Install the pinned dependencies by running this command:
pip install -r requirements.txt
What does this command install?
The -r option tells pip to read package names from requirements.txt. Each package is installed at the version recorded in that file.
The packages stay inside the active .venv environment. Your other Python projects remain separate.
- Review the final terminal lines after the prompt returns.
You'll see the pinned packages named in the installation summary. The active environment name remains visible in the terminal prompt.
Dependency installation failed?
Confirm requirements.txt contains all three pins. Also confirm the terminal reports Python 3.11 or newer.
Use this prompt with the non-secret terminal output. Help me troubleshoot the dependency installation for MovieMatch AI.
- Before you run the final check, predict what will open outside the terminal.
- Launch Streamlit's official example app by running this command:
streamlit hello
What does this command prove?
This command starts a local Streamlit server and opens its example app through your browser. A working page proves the installed package can communicate with the browser.
- Check the browser tab opened by Streamlit.
You'll see the Streamlit example app served from your computer. You have cleared the setup hurdle.
The example app did not open?
Keep the terminal running while you check for the local address printed there. Copy that address into your browser if no tab opened automatically.
Use this prompt if the page still does not load. Help me troubleshoot streamlit hello on Windows.
- Return to the Visual Studio Code terminal after capturing the browser result.
- Press Ctrl+C to stop the example server.
You'll return to the terminal prompt with .venv still active. Your Windows workspace is ready for the MovieMatch AI catalog.
Display the Movie Catalog
Your local Streamlit installation is working in the browser. MovieMatch AI now needs its first visible form.
A structured catalog gives the recommender consistent movie information to compare. This step turns the fictional movie data into a browser table that you can inspect before adding recommendation logic.
In this step, get ready to:
- Create streamlit_app.py with the complete 16-movie source data.
- Convert the catalog into a pandas DataFrame.
- Launch MovieMatch AI to verify the catalog in your browser.
Create the catalog file
Each entry in MOVIES is a dictionary containing one title. Its genres and description values become the text that later recommendation steps can compare.
- In Visual Studio Code, return to the Explorer sidebar.
- Select the movie-match-ai folder.
- Select the file creation icon beside the folder.
- Type streamlit_app.py into the filename field.
- Press Enter to create the file.
- Add the complete catalog app by copying the file from the second tab below:
✔️ Awesome, I've got everything!
Your streamlit_app.py file contains the imports, all 16 movie records, the DataFrame conversion, and the browser output.
ⓧ I'd like to double check the full code
import pandas as pd
import streamlit as st
MOVIES = [
{
"title": "Orbit's Edge",
"genres": "Sci-Fi, Adventure",
"description": "A lone salvage pilot crosses a dangerous asteroid belt to rescue a stranded research crew.",
},
{
"title": "Red Planet Rescue",
"genres": "Sci-Fi, Adventure",
"description": "A team of engineers races across Mars to repair a failing colony before a dust storm.",
},
{
"title": "Echoes of Tomorrow",
"genres": "Sci-Fi, Drama",
"description": "A linguist receives messages from the future and must choose whether to change her family history.",
},
{
"title": "Neon Pursuit",
"genres": "Sci-Fi, Thriller",
"description": "A city detective hunts a rogue android through rain-soaked streets and hidden technology markets.",
},
{
"title": "Deep Blue Signal",
"genres": "Sci-Fi, Mystery",
"description": "Ocean researchers decode a mysterious signal beneath an isolated underwater station.",
},
{
"title": "Moonlight Bakery",
"genres": "Romance, Comedy",
"description": "Two rival bakers fall in love while competing to save a small coastal town festival.",
},
{
"title": "Second Take",
"genres": "Romance, Comedy",
"description": "Former film partners reunite for a chaotic wedding and discover their old feelings.",
},
{
"title": "Letters from Winter",
"genres": "Romance, Drama",
"description": "A teacher finds decades-old love letters that connect two families in a snowy village.",
},
{
"title": "Clockwork Manor",
"genres": "Mystery, Thriller",
"description": "A young archivist investigates a locked mansion where clocks predict each new crime.",
},
{
"title": "Last Train North",
"genres": "Mystery, Drama",
"description": "Passengers on a night train uncover a missing-person mystery tied to a remote mountain town.",
},
{
"title": "Wildwood Guardians",
"genres": "Fantasy, Adventure",
"description": "Sibling explorers protect an enchanted forest from a mining company and awaken ancient spirits.",
},
{
"title": "Dragon's Apprentice",
"genres": "Fantasy, Family",
"description": "A shy apprentice and a playful dragon learn courage while defending their floating village.",
},
{
"title": "Pixel Champions",
"genres": "Animation, Comedy",
"description": "Arcade characters escape their game and form an unlikely team for a citywide tournament.",
},
{
"title": "Chef on Wheels",
"genres": "Comedy, Drama",
"description": "A struggling chef starts a food truck and rebuilds a relationship with his teenage daughter.",
},
{
"title": "Silent Summit",
"genres": "Adventure, Drama",
"description": "A climber leads a rescue mission on a frozen peak while confronting an old failure.",
},
{
"title": "Skyward Stories",
"genres": "Animation, Family",
"description": "A curious paper airplane and a young inventor travel through a colorful world above the clouds.",
},
]
movies = pd.DataFrame(MOVIES)
st.write("# MovieMatch AI")
st.write(
"Choose a fictional movie to get five recommendations based on its genres and story description."
)
st.write("## Movie catalog")
st.write(movies[["title", "genres"]])
What does this file do?
- The imports make pandas data handling and Streamlit rendering available.
- The MOVIES list stores 16 dictionaries with consistent movie fields.
- The pd.DataFrame(MOVIES) expression turns those dictionaries into rows and columns.
- The st.write calls render the app heading, instructions, and selected catalog columns.
- Save streamlit_app.py.
- Confirm that the Explorer sidebar lists streamlit_app.py inside movie-match-ai.
Seeing code errors in the editor?
Check that every movie dictionary has matching braces. Confirm that the MOVIES list ends with its closing bracket.
Ask for help checking the catalog structure.
Launch MovieMatch AI
The Streamlit command starts a local server that turns streamlit_app.py into a browser app. The terminal stays occupied while that server is running.
Before you launch the app, what information do you expect the first catalog table to show?
- Launch MovieMatch AI from the activated VS Code terminal by running this command:
streamlit run streamlit_app.py
What does this command do?
Streamlit runs streamlit_app.py as a local web app. It opens the app through your browser so you can inspect the rendered catalog.
You should see the MovieMatch AI heading above a short instruction. Below it, the movie catalog shows title and genre columns.
- Leave the Streamlit process running while you inspect the app.
Browser app not opening?
Confirm that the terminal still shows the active virtual environment. Check that streamlit_app.py is saved inside movie-match-ai.
Help me troubleshoot the local MovieMatch AI launch.
Inspect every movie record
The browser table keeps the catalog easy to scan by showing titles with genres. The descriptions remain in the source data so the recommender can use story details later.
- Return to the MovieMatch AI browser tab.
- Count the catalog rows to confirm that all 16 movies are displayed.
- Confirm that every browser row has a title and a genre string.
- Switch back to streamlit_app.py in Visual Studio Code.
- Inspect each dictionary inside MOVIES.
- Confirm that every dictionary contains title, genres, and description keys.
You should find 16 browser rows backed by 16 complete source records. That is your first working MovieMatch AI screen, with the structured text needed for recommendations.
Your movie catalog is now visible in the browser. Next, you will turn its genre text into ranked recommendations and see where a simple baseline falls short.
Build the Genre-Only Baseline
Your Streamlit catalog now makes all 16 movies visible in the browser. The DataFrame gives your recommender structured genre labels to compare.
This first content-based filtering pipeline uses only those labels. TF-IDF turns the genre text into numeric vectors.
Cosine similarity ranks the movies by closeness. You will judge this limited genre signal after seeing its recommendations.
In this step, get ready to:
- Transform the movie genres into TF-IDF features.
- Rank five movies with cosine similarity scores.
- Run the genre-only recommender in the browser.
Turn genres into comparable vectors
The scikit-learn library supplies the text vectorizer and similarity calculation. These tools convert each genre string into values that the app can compare.
- In streamlit_app.py, select the existing two-line import block.
- Replace the selected block with this complete import group:
import pandas as pd
import streamlit as st
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity
What do these imports provide?
- TfidfVectorizer converts the genre strings into a feature matrix.
- cosine_similarity measures how closely each movie vector points in the same direction as every other movie vector.
- Save streamlit_app.py.
- Return to the browser tab running your app.
The movie catalog reloads successfully. This confirms that the new imports are available inside your active virtual environment.
Imports not loading?
Confirm that the active Visual Studio Code terminal still shows .venv. An inactive virtual environment can hide the packages you installed earlier.
Help me diagnose why the scikit-learn imports in my MovieMatch AI app are failing.
The vectorizer needs one text document per movie. The genres column already provides that input.
- In streamlit_app.py, find movies = pd.DataFrame(MOVIES).
- Replace that line with the feature-building block below:
movies = pd.DataFrame(MOVIES)
vectorizer = TfidfVectorizer()
feature_matrix = vectorizer.fit_transform(movies["genres"])
similarity_matrix = cosine_similarity(feature_matrix)
How does the baseline work?
- vectorizer holds the TF-IDF converter.
- feature_matrix stores one numeric genre vector for each movie.
- similarity_matrix stores the comparison score for every pair of movies.
- Save streamlit_app.py.
- Return to the browser tab running MovieMatch AI.
The catalog renders again without a Python traceback. This proves that the genre strings can pass through the complete feature pipeline.
Catalog no longer loading?
Check that movies["genres"] uses the existing lowercase column name. Confirm that both new imports appear at the top of the file.
Help me troubleshoot the genre feature matrix in MovieMatch AI.
Rank the closest movies
A similarity matrix contains scores for every possible pair. The recommendation function selects one row from that matrix before ordering its strongest matches.
- In streamlit_app.py, place your cursor below similarity_matrix = cosine_similarity(feature_matrix).
- Add the ranking function by pasting this code:
def recommend_movies(selected_title, similarity_matrix, limit=5):
selected_index = next(
index
for index, movie in enumerate(MOVIES)
if movie["title"] == selected_title
)
scored_movies = [
(index, float(score))
for index, score in enumerate(similarity_matrix[selected_index])
if index != selected_index
]
ranked_movies = sorted(
scored_movies,
key=lambda item: item[1],
reverse=True,
)
recommendations = []
for index, score in ranked_movies[:limit]:
recommendation = MOVIES[index].copy()
recommendation["score"] = score
recommendations.append(recommendation)
return recommendations
What does this function do?
- selected_index finds the catalog position of the chosen title.
- scored_movies pairs every other movie with its similarity score.
- ranked_movies orders those pairs from the highest score to the lowest score.
- recommendations returns up to five copied movie dictionaries with their scores attached.
- Save streamlit_app.py.
- Return to the browser tab running your app.
The app reloads with the catalog still visible. This confirms that Python can define the complete ranking function.
App failing after the function edit?
Compare the indentation inside recommend_movies() with the snippet. Every line inside the function needs consistent spacing.
Help me find an indentation or syntax problem in recommend_movies().
Expose the baseline in the browser
The ranking function now needs a selected title from the learner. A select box supplies that title through the browser interface.
- In streamlit_app.py, select the interface section from st.write("# MovieMatch AI") through st.write(movies[["title", "genres"]]).
- Replace the selected section with this interface code:
movie_titles = [movie["title"] for movie in MOVIES]
st.write("# MovieMatch AI")
st.write(
"Choose a fictional movie to get five recommendations based on its genres and story description."
)
st.write("## Movie catalog")
st.write(movies[["title", "genres"]])
selected_title = st.selectbox("Choose a movie", movie_titles)
How does the selection work?
- movie_titles extracts each title from the fictional catalog.
- st.selectbox() displays those titles as browser choices.
- selected_title holds the movie currently shown in the select box.
- Save streamlit_app.py.
- Return to the browser tab running MovieMatch AI.
You will see a Choose a movie select box below the catalog. It contains every title from MOVIES.
Select box missing?
Confirm that selected_title = st.selectbox("Choose a movie", movie_titles) appears after the catalog code. Save the file again if the browser still shows the earlier interface.
Help me troubleshoot the missing movie select box.
The button completes the recommendation loop. Each result displays the title before showing its genre labels and formatted similarity score.
- In streamlit_app.py, place your cursor below selected_title = st.selectbox("Choose a movie", movie_titles).
- Add the recommendation results by pasting this code:
if st.button("Recommend movies", type="primary"):
recommendations = recommend_movies(selected_title, similarity_matrix)
st.write(f"## Movies similar to {selected_title}")
for rank, recommendation in enumerate(recommendations, start=1):
st.write(f"### {rank}. {recommendation['title']}")
st.write(f"**Genres:** {recommendation['genres']}")
st.write(f"**Similarity:** {recommendation['score']:.0%}")
st.write("---")
How are the results displayed?
- st.button() starts the recommendation calculation when you click it.
- enumerate() numbers the five ranked results from 1.
- .0% formats each decimal similarity score as a whole percentage.
- Save streamlit_app.py.
- Return to the browser tab running MovieMatch AI.
Before you test the finished baseline, which movies do you think will rank highest when genres are the only evidence?
- Select Orbit's Edge from the Choose a movie box.
- Click Recommend movies.
You will see five ranked titles with genre labels and similarity percentages. Several science-fiction results have close or matching scores even though their stories cover different situations.
That shortfall is intentional. The baseline only sees broad genre labels, so it cannot use story details to separate otherwise similar candidates.
Recommendations not appearing?
Confirm that the button block sits below the select box without extra indentation. Check that the function call uses selected_title and similarity_matrix.
Help me troubleshoot why the recommendation button produces no results.
✔️ Awesome, I've got everything!
- Confirm that streamlit_app.py is saved.
ⓧ I'd like to double check the full code
- Compare your streamlit_app.py file with this complete baseline:
import pandas as pd
import streamlit as st
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity
MOVIES = [
{
"title": "Orbit's Edge",
"genres": "Sci-Fi, Adventure",
"description": "A lone salvage pilot crosses a dangerous asteroid belt to rescue a stranded research crew.",
},
{
"title": "Red Planet Rescue",
"genres": "Sci-Fi, Adventure",
"description": "A team of engineers races across Mars to repair a failing colony before a dust storm.",
},
{
"title": "Echoes of Tomorrow",
"genres": "Sci-Fi, Drama",
"description": "A linguist receives messages from the future and must choose whether to change her family history.",
},
{
"title": "Neon Pursuit",
"genres": "Sci-Fi, Thriller",
"description": "A city detective hunts a rogue android through rain-soaked streets and hidden technology markets.",
},
{
"title": "Deep Blue Signal",
"genres": "Sci-Fi, Mystery",
"description": "Ocean researchers decode a mysterious signal beneath an isolated underwater station.",
},
{
"title": "Moonlight Bakery",
"genres": "Romance, Comedy",
"description": "Two rival bakers fall in love while competing to save a small coastal town festival.",
},
{
"title": "Second Take",
"genres": "Romance, Comedy",
"description": "Former film partners reunite for a chaotic wedding and discover their old feelings.",
},
{
"title": "Letters from Winter",
"genres": "Romance, Drama",
"description": "A teacher finds decades-old love letters that connect two families in a snowy village.",
},
{
"title": "Clockwork Manor",
"genres": "Mystery, Thriller",
"description": "A young archivist investigates a locked mansion where clocks predict each new crime.",
},
{
"title": "Last Train North",
"genres": "Mystery, Drama",
"description": "Passengers on a night train uncover a missing-person mystery tied to a remote mountain town.",
},
{
"title": "Wildwood Guardians",
"genres": "Fantasy, Adventure",
"description": "Sibling explorers protect an enchanted forest from a mining company and awaken ancient spirits.",
},
{
"title": "Dragon's Apprentice",
"genres": "Fantasy, Family",
"description": "A shy apprentice and a playful dragon learn courage while defending their floating village.",
},
{
"title": "Pixel Champions",
"genres": "Animation, Comedy",
"description": "Arcade characters escape their game and form an unlikely team for a citywide tournament.",
},
{
"title": "Chef on Wheels",
"genres": "Comedy, Drama",
"description": "A struggling chef starts a food truck and rebuilds a relationship with his teenage daughter.",
},
{
"title": "Silent Summit",
"genres": "Adventure, Drama",
"description": "A climber leads a rescue mission on a frozen peak while confronting an old failure.",
},
{
"title": "Skyward Stories",
"genres": "Animation, Family",
"description": "A curious paper airplane and a young inventor travel through a colorful world above the clouds.",
},
]
movies = pd.DataFrame(MOVIES)
vectorizer = TfidfVectorizer()
feature_matrix = vectorizer.fit_transform(movies["genres"])
similarity_matrix = cosine_similarity(feature_matrix)
def recommend_movies(selected_title, similarity_matrix, limit=5):
selected_index = next(
index
for index, movie in enumerate(MOVIES)
if movie["title"] == selected_title
)
scored_movies = [
(index, float(score))
for index, score in enumerate(similarity_matrix[selected_index])
if index != selected_index
]
ranked_movies = sorted(
scored_movies,
key=lambda item: item[1],
reverse=True,
)
recommendations = []
for index, score in ranked_movies[:limit]:
recommendation = MOVIES[index].copy()
recommendation["score"] = score
recommendations.append(recommendation)
return recommendations
movie_titles = [movie["title"] for movie in MOVIES]
st.write("# MovieMatch AI")
st.write(
"Choose a fictional movie to get five recommendations based on its genres and story description."
)
st.write("## Movie catalog")
st.write(movies[["title", "genres"]])
selected_title = st.selectbox("Choose a movie", movie_titles)
if st.button("Recommend movies", type="primary"):
recommendations = recommend_movies(selected_title, similarity_matrix)
st.write(f"## Movies similar to {selected_title}")
for rank, recommendation in enumerate(recommendations, start=1):
st.write(f"### {rank}. {recommendation['title']}")
st.write(f"**Genres:** {recommendation['genres']}")
st.write(f"**Similarity:** {recommendation['score']:.0%}")
st.write("---")
You now have a working recommendation loop that ranks movies in the browser. Next, you will enrich its feature vectors with story descriptions so the scores can reflect more than broad genre overlap.
Make Recommendations Story-Aware
Your Streamlit app now completes the recommendation loop. The genre-only baseline exposed repeated scores because broad labels miss the details that make each story distinct.
This step combines genres with descriptions in each TF-IDF vector. Cosine similarity can then rank movies using story words as well as genre overlap.
In this step, get ready to:
- Combine every movie's genres with its description.
- Vectorize the richer text through two reusable helper functions.
- Display story details before testing science-fiction and romance recommendations.
Build the story-aware feature pipeline
A movie's genres provide broad categories. Its description adds specific clues about characters, settings, conflicts, and themes.
The new feature_text column stores both sources in one DataFrame column. Two helper functions keep data preparation separate from similarity calculation.
- In streamlit_app.py, find the four setup lines directly below the MOVIES list.
- Select the block that starts with movies = pd.DataFrame(MOVIES).
- Replace the selected block with the two helper functions by pasting this code:
def build_movie_table():
movies = pd.DataFrame(MOVIES)
movies["feature_text"] = movies["genres"] + " " + movies["description"]
return movies
def build_similarity_matrix(movies):
vectorizer = TfidfVectorizer()
feature_matrix = vectorizer.fit_transform(movies["feature_text"])
return cosine_similarity(feature_matrix)
What do these helpers do?
- The build_movie_table() function converts the catalog into a DataFrame. It joins each genre string with its matching description.
- The build_similarity_matrix() function learns TF-IDF features from feature_text. It returns the cosine similarity score for every movie pair.
- Find return recommendations at the end of recommend_movies().
- Add the two helper calls directly below the function by pasting this code:
movies = build_movie_table()
similarity_matrix = build_similarity_matrix(movies)
movie_titles = [movie["title"] for movie in MOVIES]
Why call the helpers here?
The first call prepares the movie table. The second call uses that completed table to calculate similarity scores.
The title list then supplies the choices shown in the app's movie selector.
Before you test this version, do you expect movies with matching genres to keep identical scores?
- Save streamlit_app.py.
- Switch back to the browser app from earlier.
- Select Orbit's Edge from Choose a movie.
- Click Recommend movies.
You'll see five ranked results with percentages that respond to story vocabulary. Movies sharing a genre can now receive different scores.
Recommendations not loading?
Check that both helper calls sit below recommend_movies(). A call placed inside the function changes its control flow.
Check that feature_text has the same spelling in both helpers.
Ask for help with the feature pipeline.
Show why each movie matched
A recommendation score shows how strongly two feature vectors align. The description gives you a human-readable clue about which story details may have influenced that score.
- In streamlit_app.py, find the result line that displays recommendation['genres'].
- Add the description output directly below that line by pasting this code:
st.write(recommendation["description"])
What does this line add?
Each recommendation dictionary already contains its original description. st.write renders that sentence between the genres and the similarity percentage.
- Save streamlit_app.py.
- Return to the browser app from earlier.
- Select Orbit's Edge from Choose a movie.
- Click Recommend movies.
You'll see five numbered recommendations. Each result now includes its genres, description, and formatted similarity percentage.
Descriptions missing from the results?
Check that the new st.write line is inside the recommendation loop. Its indentation must match the genre and similarity lines.
Ask for help with the result display.
✔️ Awesome, I've got everything!
Your feature pipeline now matches the story-aware version.
ⓧ I'd like to double check the full code
- Compare your streamlit_app.py file with this complete version:
import pandas as pd
import streamlit as st
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity
MOVIES = [
{
"title": "Orbit's Edge",
"genres": "Sci-Fi, Adventure",
"description": "A lone salvage pilot crosses a dangerous asteroid belt to rescue a stranded research crew.",
},
{
"title": "Red Planet Rescue",
"genres": "Sci-Fi, Adventure",
"description": "A team of engineers races across Mars to repair a failing colony before a dust storm.",
},
{
"title": "Echoes of Tomorrow",
"genres": "Sci-Fi, Drama",
"description": "A linguist receives messages from the future and must choose whether to change her family history.",
},
{
"title": "Neon Pursuit",
"genres": "Sci-Fi, Thriller",
"description": "A city detective hunts a rogue android through rain-soaked streets and hidden technology markets.",
},
{
"title": "Deep Blue Signal",
"genres": "Sci-Fi, Mystery",
"description": "Ocean researchers decode a mysterious signal beneath an isolated underwater station.",
},
{
"title": "Moonlight Bakery",
"genres": "Romance, Comedy",
"description": "Two rival bakers fall in love while competing to save a small coastal town festival.",
},
{
"title": "Second Take",
"genres": "Romance, Comedy",
"description": "Former film partners reunite for a chaotic wedding and discover their old feelings.",
},
{
"title": "Letters from Winter",
"genres": "Romance, Drama",
"description": "A teacher finds decades-old love letters that connect two families in a snowy village.",
},
{
"title": "Clockwork Manor",
"genres": "Mystery, Thriller",
"description": "A young archivist investigates a locked mansion where clocks predict each new crime.",
},
{
"title": "Last Train North",
"genres": "Mystery, Drama",
"description": "Passengers on a night train uncover a missing-person mystery tied to a remote mountain town.",
},
{
"title": "Wildwood Guardians",
"genres": "Fantasy, Adventure",
"description": "Sibling explorers protect an enchanted forest from a mining company and awaken ancient spirits.",
},
{
"title": "Dragon's Apprentice",
"genres": "Fantasy, Family",
"description": "A shy apprentice and a playful dragon learn courage while defending their floating village.",
},
{
"title": "Pixel Champions",
"genres": "Animation, Comedy",
"description": "Arcade characters escape their game and form an unlikely team for a citywide tournament.",
},
{
"title": "Chef on Wheels",
"genres": "Comedy, Drama",
"description": "A struggling chef starts a food truck and rebuilds a relationship with his teenage daughter.",
},
{
"title": "Silent Summit",
"genres": "Adventure, Drama",
"description": "A climber leads a rescue mission on a frozen peak while confronting an old failure.",
},
{
"title": "Skyward Stories",
"genres": "Animation, Family",
"description": "A curious paper airplane and a young inventor travel through a colorful world above the clouds.",
},
]
def build_movie_table():
movies = pd.DataFrame(MOVIES)
movies["feature_text"] = movies["genres"] + " " + movies["description"]
return movies
def build_similarity_matrix(movies):
vectorizer = TfidfVectorizer()
feature_matrix = vectorizer.fit_transform(movies["feature_text"])
return cosine_similarity(feature_matrix)
def recommend_movies(selected_title, similarity_matrix, limit=5):
selected_index = next(
index
for index, movie in enumerate(MOVIES)
if movie["title"] == selected_title
)
scored_movies = [
(index, float(score))
for index, score in enumerate(similarity_matrix[selected_index])
if index != selected_index
]
ranked_movies = sorted(
scored_movies,
key=lambda item: item[1],
reverse=True,
)
recommendations = []
for index, score in ranked_movies[:limit]:
recommendation = MOVIES[index].copy()
recommendation["score"] = score
recommendations.append(recommendation)
return recommendations
movies = build_movie_table()
similarity_matrix = build_similarity_matrix(movies)
movie_titles = [movie["title"] for movie in MOVIES]
st.write("# MovieMatch AI")
st.write(
"Choose a fictional movie to get five recommendations based on its genres and story description."
)
st.write("## Movie catalog")
st.write(movies[["title", "genres"]])
selected_title = st.selectbox("Choose a movie", movie_titles)
if st.button("Recommend movies", type="primary"):
recommendations = recommend_movies(selected_title, similarity_matrix)
st.write(f"## Movies similar to {selected_title}")
for rank, recommendation in enumerate(recommendations, start=1):
st.write(f"### {rank}. {recommendation['title']}")
st.write(f"**Genres:** {recommendation['genres']}")
st.write(recommendation["description"])
st.write(f"**Similarity:** {recommendation['score']:.0%}")
st.write("---")
What should match?
The two helper functions should appear between the movie catalog and recommend_movies(). Their calls should appear below the recommendation function.
The result loop should display each description before its similarity percentage.
Test science fiction and romance
The science-fiction result proves that story words can separate movies with overlapping genres. A romance test checks that the same pipeline works in another part of the catalog.
Before the final check, which story details do you expect to shape the romance ranking?
- Select Orbit's Edge from Choose a movie.
- Click Recommend movies.
You'll see five science-fiction recommendations with numbered titles. Every result includes a description and a story-aware similarity percentage.
- Select Moonlight Bakery from Choose a movie.
- Click Recommend movies.
You'll see another ranked list with descriptions and similarity percentages. The remaining order reflects romance story vocabulary as well as genre overlap.
That's the core recommender working. MovieMatch AI now uses richer text to produce explainable ranked suggestions across the catalog.
Secret mission
Add a Genre Filter
Your recommender already ranks movies by story-aware similarity. Add an optional genre filter that narrows those ranked results while preserving the original score order.
Clean Up Your Resources
Clean Up Your Resources
Your Streamlit app runs entirely on your computer, so keeping the local files creates no ongoing costs. Choose whether to keep the app running, pause it for later, or delete the local project.
Resources you used:
- The local Streamlit server process running from the Visual Studio Code terminal.
- The movie-match-ai folder containing streamlit_app.py, requirements.txt, and .venv.
Keep everything running
No action is needed. Choose this option if you are still testing recommendations or changing the movie catalog.
- Leave the local Streamlit server running in the Visual Studio Code terminal.
- Keep the browser tab available for testing MovieMatch AI.
- Keep the movie-match-ai folder in its current location.
- Expect no ongoing charges because the app uses only local files and processes.
Pause - I'll come back to this later
Stop the local server to free your computer's memory. Your code and virtual environment stay in place for your next session.
- Switch back to the terminal panel in Visual Studio Code.
- Press Ctrl+C to stop the Streamlit server.
- Close the MovieMatch AI browser tab.
- Leave the movie-match-ai folder in its current location.
The terminal prompt returns when the server stops. Your source files and .venv environment remain ready for later.
Delete - I don't want to use this again
Remove the running process and the complete local project. Follow each action below to delete everything created for MovieMatch AI.
Deleting the folder permanently removes the catalog, recommender, and virtual environment. Your system-wide Python and Visual Studio Code installations stay untouched.
- Switch back to the terminal panel in Visual Studio Code.
- Press Ctrl+C to stop the Streamlit server.
- Close Visual Studio Code.
The local server is now stopped. The remaining resource is the movie-match-ai folder.
- Press the Windows key to open Windows search.
- Type File Explorer into the search field.
- Press Enter to open File Explorer.
- Locate the same movie-match-ai folder that you opened earlier.
- Delete the entire movie-match-ai folder.
- Empty the Recycle Bin to remove the project permanently.
- Search for movie-match-ai in File Explorer.
You should see no matching project folder. This confirms that streamlit_app.py, requirements.txt, and the .venv environment have been removed.
Nice Work!
Nice Work!
Mission accomplished! You built MovieMatch AI as a local Streamlit app that ranks five related movies using genres plus story descriptions.
What you learned:
- Built a browser-based content-based recommender that turns one selected movie into five ranked suggestions with visible similarity scores.
- Prepared structured movie data in a pandas DataFrame. Transformed genres plus descriptions into TF-IDF features.
- Experienced the limits of a genre-only baseline through repetitive matches. Improved the ranking with story-aware features plus cosine similarity scores.
- Secret Mission: Extended the recommender with an optional genre filter that preserves score order among matching movies.
Ready to quiz yourself?