Free tutorials & notes in Hindi & English · Clean code examples 🧰 Free PDF & Image Tools →
⚙️ Week 8 – Advanced ML  ·  Day 48

Day 48: NLP: Text Cleaning & Sentiment Model

⏱ 2.5 hours📶 Intermediate🎯 100% self-study
Day 48 / 60

✅ Today you will learn

  • How computers “read” text
  • Clean, tokenise and remove stop words
  • Bag of words, TF-IDF and n-grams
  • Train a sentiment classifier in a pipeline
  • Explain predictions and know the limits

🏢 In the real world

BazaarKart receives thousands of reviews a day. The quality team cannot read them all. A sentiment model flags negative reviews within seconds and routes them to support – and the top negative words tell product teams exactly what to fix.

From words to numbers

Raw reviewClean (lowercase, remove symbols)Tokens (words)Vector (counts or TF-IDF)Classifier → positive / negative
The classic NLP pipeline
Python
import re
import pandas as pd
r = pd.read_csv("product_reviews.csv")
print(r["sentiment"].value_counts().to_dict())
def clean(t):
    t = t.lower()
    t = re.sub(r"[^a-z\s]", " ", t)          # keep letters only
    return re.sub(r"\s+", " ", t).strip()
r["clean"] = r["review"].apply(clean)
print(r[["review", "clean"]].head(4).to_string(index=False))
{'positive': 348, 'negative': 252} review clean Colour faded after first wash One star. colour faded after first wash one star Fabric is soft and colour is same as photo fabric is soft and colour is same as photo Product is fake, not original Never again. product is fake not original never again Very poor quality, stopped working in a week very poor quality stopped working in a week

Bag of words

Python
from sklearn.feature_extraction.text import CountVectorizer
docs = ["battery life is amazing", "battery died fast, very bad", "amazing sound, amazing price"]
cv = CountVectorizer()
M = cv.fit_transform(docs)
import pandas as pd
print(pd.DataFrame(M.toarray(), columns=cv.get_feature_names_out(), index=["r1", "r2", "r3"]))
amazing bad battery died fast is life price sound very r1 1 0 1 0 0 1 1 0 0 0 r2 0 1 1 1 1 0 0 0 0 1 r3 2 0 0 0 0 0 0 1 1 0
🧠 Word order is lost

Bag of words only counts words – “not good” and “good, not bad” look similar. N-grams (pairs of words) bring back some order: ngram_range=(1, 2).

TF-IDF: important words count more

TF = how often a word appears in this review. IDF = how rare it is across all reviews. Words like “the” and “is” appear everywhere and get low weight; “refund” or “cracks” get high weight.

Python
import pandas as pd
from sklearn.feature_extraction.text import TfidfVectorizer
r = pd.read_csv("product_reviews.csv")
tf = TfidfVectorizer(stop_words="english", ngram_range=(1, 2), min_df=2)
X = tf.fit_transform(r["review"])
print("matrix:", X.shape, "| sparsity:", round(1 - X.nnz / (X.shape[0] * X.shape[1]), 3))
row = pd.Series(X[0].toarray().ravel(), index=tf.get_feature_names_out())
print("review 0:", r.loc[0, "review"]); print(row[row > 0].sort_values(ascending=False).round(3))
matrix: (600, 186) | sparsity: 0.958 review 0: Colour faded after first wash One star. wash star 0.495 faded 0.376 colour faded 0.376 wash 0.376 faded wash 0.376 star 0.317 colour 0.296 dtype: float64

Train the sentiment classifier

Python
import pandas as pd
from sklearn.model_selection import train_test_split, cross_val_score
from sklearn.pipeline import make_pipeline
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.naive_bayes import MultinomialNB
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import classification_report
r = pd.read_csv("product_reviews.csv")
X_tr, X_te, y_tr, y_te = train_test_split(r["review"], r["sentiment"], test_size=0.25, random_state=0, stratify=r["sentiment"])
for name, clf in [("Naive Bayes", MultinomialNB()), ("Logistic regression", LogisticRegression(max_iter=2000))]:
    pipe = make_pipeline(TfidfVectorizer(ngram_range=(1, 2), min_df=2), clf)
    print(f"{name:20s} 5-fold CV accuracy = {cross_val_score(pipe, X_tr, y_tr, cv=5).mean():.3f}")
best = make_pipeline(TfidfVectorizer(ngram_range=(1, 2), min_df=2), LogisticRegression(max_iter=2000)).fit(X_tr, y_tr)
print(classification_report(y_te, best.predict(X_te), digits=3))
Naive Bayes 5-fold CV accuracy = 1.000 Logistic regression 5-fold CV accuracy = 1.000 precision recall f1-score support negative 1.000 1.000 1.000 63 positive 1.000 1.000 1.000 87 accuracy 1.000 150 macro avg 1.000 1.000 1.000 150 weighted avg 1.000 1.000 1.000 150
🔎 Why 100%?

Our practice reviews are short and use clear words, so the task is easy and the score is perfect. Real review data with typos, mixed opinions and sarcasm usually gives 85–92%. A perfect score on real data should make you check for leakage.

Which words drive sentiment?

Python
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.pipeline import make_pipeline
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.linear_model import LogisticRegression
r = pd.read_csv("product_reviews.csv")
pipe = make_pipeline(TfidfVectorizer(stop_words="english", min_df=2), LogisticRegression(max_iter=2000)).fit(r["review"], r["sentiment"])
coef = pd.Series(pipe[-1].coef_[0], index=pipe[0].get_feature_names_out())      # positive class = "positive"
top = pd.concat([coef.nsmallest(10), coef.nlargest(10)])
plt.figure(figsize=(8, 5.2))
plt.barh(top.index, top.values, color=["#dc2626" if v < 0 else "#16a34a" for v in top.values])
plt.title("Words that push a review negative (red) or positive (green)"); plt.xlabel("coefficient")
plt.show()
Bar chart of most negative and positive words by coefficient
Red words are the complaint list for the product team.

Score new reviews – and find the limits

Python
import pandas as pd
from sklearn.pipeline import make_pipeline
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.linear_model import LogisticRegression
r = pd.read_csv("product_reviews.csv")
pipe = make_pipeline(TfidfVectorizer(ngram_range=(1, 2), min_df=2), LogisticRegression(max_iter=2000)).fit(r["review"], r["sentiment"])
tests = ["Battery life is amazing, loved it", "Stopped working in a week, waste of money",
         "Not bad at all, quite good actually", "delivery late but product is superb",
         "bilkul bekaar product, paisa barbaad", "Phone is ok"]
for t, p in zip(tests, pipe.predict_proba(tests)[:, list(pipe.classes_).index("positive")]):
    print(f"{p:5.2f}  {'POS' if p >= 0.5 else 'NEG'}  {t}")
0.93 POS Battery life is amazing, loved it 0.13 NEG Stopped working in a week, waste of money 0.15 NEG Not bad at all, quite good actually 0.60 POS delivery late but product is superb 0.59 POS bilkul bekaar product, paisa barbaad 0.48 NEG Phone is ok
⚠️ What the model does not understand

Negation (“not bad”), mixed reviews, sarcasm and Hinglish words it has never seen (“bekaar”, “barbaad”) confuse a bag-of-words model – unseen words are simply ignored. Fixes: collect labelled Hinglish reviews, add n-grams, or use multilingual transformer models (Day 51).

Real example: complaint topics

Python
import pandas as pd
r = pd.read_csv("product_reviews.csv")
neg = r[r["sentiment"] == "negative"]["review"].str.lower()
topics = {"battery / charging": "battery|charg", "quality / broken": "broke|stopped|crack|faded|quality",
          "size / fit": "size|fit|tight|small", "fake / value": "fake|waste|money|original",
          "delivery": "deliver|late|courier"}
counts = {k: int(neg.str.contains(v).sum()) for k, v in topics.items()}
s = pd.Series(counts).sort_values(ascending=False)
for topic, n in s.items():
    print(f"{topic:20s} {n:3d} reviews  ({n / len(neg) * 100:.1f}% of negative)")
quality / broken 73 reviews (29.0% of negative) fake / value 55 reviews (21.8% of negative) delivery 27 reviews (10.7% of negative) battery / charging 25 reviews (9.9% of negative) size / fit 24 reviews (9.5% of negative)

🏅 Job skills unlocked today

✔ Clean and tokenise text✔ Vectorise with CountVectorizer and TF-IDF✔ Build a text classification pipeline✔ Explain models with top words and test their limits

Practice tasks

🏋️ Practice – do it yourself

Task 1: Predict the star rating (1–5) instead of positive/negative. How accurate is it, and which stars get confused?
Show solution
Python
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.pipeline import make_pipeline
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import confusion_matrix
r = pd.read_csv("product_reviews.csv")
a, b, c, d = train_test_split(r["review"], r["stars"], test_size=0.25, random_state=0, stratify=r["stars"])
m = make_pipeline(TfidfVectorizer(ngram_range=(1, 2)), LogisticRegression(max_iter=3000)).fit(a, c)
print(round(m.score(b, d), 3)); print(confusion_matrix(d, m.predict(b)))
0.52 [[21 12 0 0] [18 12 0 0] [ 0 0 15 25] [ 0 0 17 30]]
Task 2: Add five Hinglish negative and five Hinglish positive reviews to the training data and re-test “bilkul bekaar product”.
Task 3: Use CountVectorizer instead of TF-IDF with Naive Bayes. Does accuracy change?
Show solution
Python
import pandas as pd
from sklearn.model_selection import cross_val_score
from sklearn.pipeline import make_pipeline
from sklearn.feature_extraction.text import CountVectorizer
from sklearn.naive_bayes import MultinomialNB
r = pd.read_csv("product_reviews.csv")
print(round(cross_val_score(make_pipeline(CountVectorizer(), MultinomialNB()), r["review"], r["sentiment"], cv=5).mean(), 3))
1.0

Quick quiz

🧠 Quick Quiz – test yourself

1. TF-IDF gives low weight to…
IDF is low for common words.
2. ngram_range=(1, 2) adds…
e.g. “not good”.
3. Stop words are…
Often removed.
4. A word never seen in training is…
It has no column.
5. Why put TfidfVectorizer in a pipeline?
Prevents leakage.

Interview corner

💼 Interview Corner

Explain TF-IDF.

Term frequency × inverse document frequency: a word’s weight rises with how often it appears in a document and falls with how many documents contain it, so distinctive words count more than common ones.

How would you handle Hindi/Hinglish reviews?

Collect labelled examples in that language, normalise spellings (bekar/bekaar), use character n-grams which are robust to spelling variants, or use multilingual models like IndicBERT or mBERT fine-tuned on the data.

Summary

  • Text → clean → tokens → TF-IDF vectors → classifier.
  • n-grams keep some word order; stop words are optional.
  • Logistic regression coefficients show the words that drive sentiment.
  • Bag-of-words cannot handle negation, sarcasm or unseen Hinglish well.
➡️ Next

Day 49: time-series forecasting – trend, seasonality and predicting next month’s Delhi AQI.

Frequently Asked Questions

Is this how ChatGPT-style models work?
No – large language models use neural networks (transformers) that understand word order and context. TF-IDF + logistic regression is still a strong, fast baseline used widely in industry.
Should I always remove stop words?
Not for sentiment – “not” is a stop word in many lists but it flips meaning. Test both ways.
← Data Science (60 Days)
🔗

Share this topic with a friend

यह topic किसी दोस्त को भेजें

Found it useful? Send it to a classmate learning the same thing.

अच्छा लगा? जो दोस्त यही सीख रहा है, उसे भेज दीजिए।

Next topicDay 49: Time Series & Forecasting Your progress: 48 / 60 (80%) · Lesson 49 / 60 Continue →

How was this lesson?

Your feedback helps us make the content better.

Write a review or suggestion (optional)

After approval, your name and city may appear publicly with your review. Please do not write your email or phone number.

▶ CodingEasily on YouTube

Learn with video lessons in easy Hindi

Featured video: RDBMS Part 3 – Class XI Commerce IT

▶ Subscribe – it’s free