Guide
Sentiment analysis with Python: approaches and trade-offs
To do sentiment analysis in Python, start with a pretrained transformer through Hugging Face for general text, VADER for fast scoring of short social posts, a trained scikit-learn model when you have labels and need cheap, explainable predictions, or an LLM when you need sentiment per topic. Test the options on your own labelled text, as a model trained on another domain may mislead you.
Define what you are measuring
"Sentiment" can mean several different tasks, and the right tool depends on which one you have. Decide before writing any code:
- Polarity. Is the text positive, negative or neutral? Many pretrained models handle only this. Check whether yours has a neutral class at all, because a two-label model must force an undecided text into one side.
- Intensity. How strong is the feeling, perhaps on a scale from one to five?
- Aspect-based sentiment. "The food was great but the delivery was late" holds two opposite opinions. A single overall label hides what you most need to know.
- Emotion. Anger, confusion and disappointment are all negative, but each may call for a different response.
Sentiment analysis is a special case of classification, so the general reasoning in text classification with LLMs vs traditional NLP applies. This guide concentrates on the options and pitfalls specific to sentiment. All code samples are illustrative; check each library's current documentation before relying on exact details.
Option 1: lexicon-based scoring with VADER
A lexicon method looks words up in a list of scores and combines them. VADER, available through NLTK, is a well-known example designed with social media text in mind. Its rules take account of features such as negation, capitals and punctuation.
# Illustrative only. The lexicon must be downloaded once.
import nltk
from nltk.sentiment.vader import SentimentIntensityAnalyzer
nltk.download("vader_lexicon")
analyzer = SentimentIntensityAnalyzer()
scores = analyzer.polarity_scores("The update is GREAT, but setup was not easy.")
print(scores) # A dictionary with neg, neu, pos and compound keys
The compound value is a normalised score between -1 and 1. Any cut-offs you apply to create labels are your own choice, so tune them on labelled examples.
Strengths. It needs no training or GPU, it is very fast and its behaviour is easy to inspect. Weaknesses. It reads words rather than meaning, so sarcasm and implied opinions can defeat it. Domain vocabulary is another issue: "unpredictable" may be praise for a film and a complaint about a car's brakes. It also targets English. It is a reasonable baseline and sometimes enough for rough trends, but a poor choice for decisions about individual texts.
Option 2: a classic classifier trained on your labels
TF-IDF features with logistic regression remain a strong baseline when you hold labelled examples from your own domain. Review sites and support systems often provide useful labels already, in the form of star ratings or resolved tickets, though ratings do not always match the words of a review.
# Illustrative only. train_texts and train_labels are your own data.
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.linear_model import LogisticRegression
from sklearn.pipeline import make_pipeline
model = make_pipeline(
TfidfVectorizer(ngram_range=(1, 2), min_df=2),
LogisticRegression(max_iter=1000, class_weight="balanced"),
)
model.fit(train_texts, train_labels)
probabilities = model.predict_proba(["Delivery was not as quick as promised"])
Using word pairs through ngram_range=(1, 2) helps the model to pick up phrases such as "not good". Setting class_weight="balanced" helps when one class is far more common than the others, which is typical of review data.
Strengths. Training and prediction are cheap, it runs on ordinary hardware and the learned weights show which terms drive a decision. It also learns your domain's vocabulary. Weaknesses. It needs labelled data, handles word order only crudely and can degrade as language or products change. Retrain it and watch its performance over time.
Option 3: a pretrained transformer
Transformers use context, so they often handle negation and phrasing better than the first two options. The Hugging Face pipeline makes it possible to try one in a few lines. The task name "sentiment-analysis" is an alias of "text-classification", and the result is a list of dictionaries containing a label and a score.
# Illustrative only. Replace the placeholder with a model chosen from its model card.
from transformers import pipeline
classifier = pipeline("sentiment-analysis", model="your-chosen-model")
results = classifier(
["The battery lasts for days.", "Support never replied."],
truncation=True,
)
for result in results:
print(result["label"], result["score"])
Name the model explicitly, and pin its revision if you are going to production, rather than relying on a default that could change. Read the model card with care. It tells you what the model was trained on, and this matters more than almost anything else: a model trained on film reviews or tweets may perform quite differently on bank complaints. It also tells you which languages and labels it supports, and label names vary between models.
Strengths. You get good handling of context with no labelling of your own, and there is a wide choice of models, including multilingual ones. Weaknesses. It is heavier to run, and may need a GPU at volume. Decisions are harder to explain, and the score is not necessarily a calibrated probability. If a pretrained model comes close but struggles with your domain, fine-tuning on your own labels is the next step, at the cost of more data and infrastructure.
Option 4: an LLM with a structured prompt
An LLM is the most flexible option, especially for aspect-based sentiment, where other methods need dedicated models and labelled data. You describe the labels and aspects in a prompt and ask for structured output:
# Illustrative only. call_llm is your wrapper around a model provider.
from typing import Literal
from pydantic import BaseModel
class AspectSentiment(BaseModel):
aspect: Literal["product", "delivery", "price", "support"]
sentiment: Literal["positive", "negative", "neutral"]
evidence: str
class ReviewAnalysis(BaseModel):
aspects: list[AspectSentiment]
def analyse_review(text: str) -> ReviewAnalysis:
raw_json = call_llm(
instructions="Report only aspects the review mentions. "
"Quote the words that support each judgement in evidence.",
user_text=text,
)
return ReviewAnalysis.model_validate_json(raw_json)
The schema keeps results usable, and the evidence field lets you check each judgement against the original text. Validate the quote programmatically if it matters. The guide on structured output from LLMs covers this further.
Strengths. It needs little or no training data at the start, adapts quickly to new aspects, and often copes with indirect language. Weaknesses. Each call adds cost and latency, which can dominate at high volume; see controlling LLM cost and latency. Text usually goes to a third party unless you self-host, which matters for reviews and tickets containing personal data. Model updates can shift results, which would corrupt a trend line, so pin versions where possible and re-test when they change.
Comparing the options
These are general tendencies. Your own data can overturn any of them.
| Factor | VADER | TF-IDF classifier | Pretrained transformer | LLM |
|---|---|---|---|---|
| Labels needed to start | None | Yes | None for inference | None for inference |
| Cost per text | Very low | Very low | Low to moderate | Usually highest |
| Handling context | Weak | Limited | Good | Often good |
| Domain fit | General, editable lexicon | Learned from your data | Depends on training data | Adjustable by prompt |
| Aspect-level results | No | Needs extra design | Needs a suitable model | Straightforward |
| Stability over time | Fixed | Fixed until retrained | Fixed if pinned | Depends on provider versions |
How to compare them fairly
Do not rely on published accuracy figures, which describe someone else's data. The test that counts uses your own:
- Label a sample of real texts. Include hard cases, and have two people label a portion independently. Where they disagree often, the task definition needs work, and that level of disagreement is a practical ceiling on what you can expect from a model.
- Hold the test set back. Never tune thresholds or prompts on the same examples you use to report results.
- Map labels carefully. A model's outputs must be mapped consistently to your classes, including what counts as neutral.
- Look beyond accuracy. If most reviews are positive, a model that always says "positive" appears accurate and is useless. Use per-class precision and recall, and macro F1, along with a confusion matrix.
- Read the errors. A pattern of mistakes, such as failing on a product name, tells you more than a score does.
# Illustrative only. true_labels and predicted_labels come from your test set.
from sklearn.metrics import classification_report, confusion_matrix
print(classification_report(true_labels, predicted_labels, digits=3))
print(confusion_matrix(true_labels, predicted_labels))
Pitfalls specific to sentiment
- Sarcasm and implied opinion. "Great, another delay" and "I had to call them four times" contain few obvious signals. Include examples like these in your tests.
- Mixed texts. One overall label for a long review may mean very little. Consider splitting by sentence or by aspect.
- Whose sentiment, about what? "The rival's product failed" is negative language but might be good news for you. Identifying the target may call for entity extraction first.
- Language and dialect. Mixed-language text, slang and emoji can all reduce performance. Report results for each group that matters, not only overall.
- Bias. Models can associate certain names or identity terms with negative sentiment. If results affect people, test for this directly.
- Trends built on changing models. A change in the tool can look like a change in customer opinion. Re-score a fixed sample when you change the model, so that you can separate the two effects.
A sensible progression
Start with the cheapest method that could plausibly work and a labelled test set. Move up only when the errors justify it. Methods can also be combined: a fast model can handle the bulk of the text, and an LLM can take low-confidence cases or provide aspect detail on a sample.
If you need help choosing or building one of these, the NLP development page describes what I can help with.
Frequently asked questions
What is the best Python library for sentiment analysis?
There is no single best library. NLTK's VADER suits quick scoring of short social text, scikit-learn suits classifiers trained on your own labels, and Hugging Face Transformers offers pretrained models. Compare them on a labelled sample of your own text.
Is an LLM better than a transformer model for sentiment?
Not automatically. An LLM is more flexible, particularly for sentiment by aspect, but it usually costs more per text and adds latency. A dedicated model may be just as good for plain polarity at high volume, so test both.
How much labelled data do I need?
For choosing among pretrained options, you only need enough labelled examples to compare them reliably, including each class and your hard cases. Training your own model needs more, and the amount depends on the task, so watch how test results change as you add data.
Can sentiment models detect sarcasm?
Sometimes, but it remains a weak spot for every approach. Lexicons are the most easily fooled, and context-aware models do better without being reliable. Include sarcastic examples from your own data in the test set to see how a model copes.