A web application that classifies movie reviews into Positive 😊, Neutral 😐, or Negative 😠 using Natural Language Processing (NLP) and Machine Learning — a Flask frontend backed by a TF-IDF + Logistic Regression model trained on ~40,000 movie reviews.
🚀 Live demo: https://moviesentimentanalysis.vercel.app
- Real-time classification — pick any movie poster, write a review for that film, and get the sentiment instantly
- 3-class prediction — Positive / Neutral / Negative with the raw label in the API response
- Explainable results — the API returns the model's confidence and the top words that drove the prediction, so you can judge whether the answer makes sense
- Negation-aware — tokens following negation words are kept as
not_bad-style features, so "not bad" is no longer read as plain "bad" - Friendly validation — emoji-only, star-rating, or numeric reviews get a plain-English explanation instead of a cryptic error
- Guided demo — one-click example reviews, a colour-coded legend, and a confidence meter in the web UI
- Robust preprocessing — shared pipeline for training and inference (no train/serve skew): HTML tag & URL stripping, non-alphabetical filtering, English + Indonesian stopword removal
- Defensible labelling — polarity comes from the original binary labels, VADER scores on the negation-preserving raw text, and word-boundary sentiment keywords; reviews without strong sentiment become the Neutral class
- No data leakage — duplicate reviews are dropped before the train/test split, TF-IDF is fitted on the training fold only, and minority-class oversampling happens on the training fold only
- Honest evaluation — headless report generator producing accuracy, classification report, confusion matrix, class distribution, and per-class word clouds
- Production-ready Flask — model paths resolved relative to the script, NLTK resources bundled in
nltk_data/(works fully offline), validated JSON input, and debug mode controlled via environment variables
- Flask — web framework
- scikit-learn — TF-IDF vectorizer & Logistic Regression
- VADER (NLTK) — lexicon-based labelling signal
- imbalanced-learn — minority-class oversampling
- pandas / NumPy — data handling
- Matplotlib / WordCloud — evaluation visuals
- Vanilla HTML / CSS / JavaScript — review interface
| Metric | Value |
|---|---|
| Accuracy | 85.59% |
| Class | Negative (F1 0.87) · Neutral (F1 0.32) · Positive (F1 0.88) |
| Split | Stratified 70% train / 30% test |
The confusion matrix shows how predictions line up against the true labels:
The Neutral class is small (only ~2.7% of the dataset), so its F1 score is naturally lower — the model is confident on clear positive/negative reviews and honest (lower confidence) on ambiguous ones.
- Create and activate a virtual environment
python -m venv venv venv\Scripts\activate # Windows # source venv/bin/activate # Linux / macOS
- Install dependencies
pip install -r requirements.txt
requirements.txtcontains only the runtime packages the web app needs. To retrain or evaluate the model, installrequirements-dev.txtinstead (pip install -r requirements-dev.txt), which adds pandas, matplotlib, WordCloud, and imbalanced-learn on top. - Start the server
python app.py
- Open http://localhost:5000 in your browser
The trained model and vectorizer ship with the repository (models/), so the app works out of the box.
- Install the full set of dependencies (training packages included):
pip install -r requirements-dev.txt
- Download the labelled movie review dataset and place it at
data/movie.csv(columns:text,label— see the Dataset section). - Train:
python scripts/train_model.py
- Evaluate:
Figures and a text report are written to
python scripts/evaluate_model.py
output/.
The app deploys to Vercel as a zero-config Flask
serverless function (Python 3.13). NLTK stopwords and the VADER lexicon ship
with the repository in nltk_data/, so no downloads happen at runtime.
npm i -g vercel
vercel login
vercel --prodMovieSentimentAnalysis/
├── app.py # Flask web server (entry point)
├── data/
│ ├── movie_reviews_sample.csv # 50-row preview of the raw dataset format
│ ├── test.csv # stratified 30% hold-out split
│ └── train.csv # stratified 70% training split
├── models/
│ ├── sentiment_model.pkl # trained Logistic Regression classifier
│ └── tfidf_vectorizer.pkl
├── nltk_data/ # vendored NLTK resources (offline cold start)
│ ├── corpora/stopwords/ # English + Indonesian stopword lists
│ └── sentiment/vader_lexicon.zip
├── output/ # evaluation figures + report (also the README images)
│ ├── confusion_matrix.png
│ ├── sentiment_distribution.png
│ ├── wordcloud_-1.png / wordcloud_0.png / wordcloud_1.png
│ ├── screenshot.jpg
│ └── evaluation_report.txt
├── scripts/
│ ├── train_model.py # label + clean + split + train + save
│ └── evaluate_model.py # metrics, confusion matrix, word clouds
├── sentiment_analysis/
│ ├── __init__.py
│ └── preprocessing.py # shared cleaning (used by app & scripts)
├── static/ # CSS, JS, movie poster art
│ └── Movie/ # page background + 6 movie posters
├── templates/
│ └── index.html
├── LICENSE
├── README.md
├── requirements.txt # runtime deps (deployed to Vercel)
├── requirements-dev.txt # runtime + training/eval deps
├── vercel.json # Vercel function config
├── .python-version # Python 3.13
└── .vercelignore # excludes data/ & output/ from uploads
The model is trained on ~40,000 publicly available movie reviews. The raw dataset is not committed to this repository (it is a large intermediate artifact); the standard practice for research repositories is to link the source and ship the processed splits, which is what this repo does:
data/movie_reviews_sample.csv— a 50-row preview of the exact raw format (text,label).data/train.csv/data/test.csv— the cleaned, labelled, deduplicated 70/30 splits used by the evaluation script.
Place the full movie.csv in data/ and run scripts/train_model.py to
reproduce the whole pipeline from raw data.
After labelling, most reviews fall into Positive or Negative; Neutral is the small minority (~2.7%). This is why the Neutral F1 score is lower.
These word clouds were generated from the training text and show what the model has actually learned to associate with each class:
All randomness is fixed via random_state=42. The training script:
- Cleans text with the shared
sentiment_analysis.preprocessingpipeline (stopwords removed, but negation is preserved asnot_*tokens). - Builds the 3-class target (polarity from the original labels, Neutral from absence of strong sentiment via VADER + word-boundary keywords).
- Drops empty and duplicate reviews before the split.
- Stratified 70/30 split, TF-IDF fitted on train only.
- Oversamples the minority Neutral class on train only.
- Trains Logistic Regression with balanced class weights and saves the model.
POST /predict with a JSON body {"text": "..."}:
{
"prediction": "Positive",
"label": 1,
"confidence": 0.978,
"words": [{ "word": "fantastic", "score": 1.52 }, { "word": "loved", "score": 1.31 }]
}confidence is the model's probability for the predicted class; words are
the tokens (and negated not_* phrases) that most influenced the decision.
Non-word inputs (emojis, star ratings, numbers) return a 422 with a
human-readable explanation.
This project is licensed under the MIT License — see the LICENSE file for details.
Melvin (@CodeMelvin)





