Skip to content
Rishad Rahman
All work

Machine learning · Applied ML study

Drug Review Analysis

An NLP and machine-learning study of patient drug reviews: a TF-IDF sentiment model, engineered features, and a Random Forest vs MLP comparison.

Stack

  • Python
  • pandas
  • scikit-learn
  • spaCy
  • NLTK
  • PyTorch
  • seaborn
  • Google Colab
reviews cleaned for the sentiment model
320,753
reviews cleaned for the sentiment model
sentiment test accuracy, positive vs negative
83.5%
sentiment test accuracy, positive vs negative
lisinopril reviews in the main study
6,524
lisinopril reviews in the main study
models compared: Random Forest and MLP
2
models compared: Random Forest and MLP

What it does

The objective was to investigate how textual sentiment and structured attributes can be combined to improve predictive performance. The data is the drugs.com side-effects review dataset: each row is one patient review with the drug, condition, age group, sex, date, ratings for effectiveness, ease of use and satisfaction, the review text, and how many readers found it useful.

The work has two parts. First, every review in the dataset is cleaned and used to train a sentiment model. Then the study narrows to a single drug, lisinopril, and combines that sentiment with engineered features to predict a five-level side-effect score, comparing a Random Forest with an MLP written in PyTorch.

1 · Sentiment model (all reviews)

drugs.com reviews
Text + ratings for every drug in the dataset
Clean text
Emoticons → words · punctuation · stopwords · spaCy lemmas
TF-IDF → Random Forest
Negative (< 3) vs positive (> 3) · 83.5% test accuracy

2 · Side-effect study (lisinopril)

6,524 reviews
One drug, after removing empty entries
Feature engineering
Date parts · Box-Cox · one-hot · scaled effectiveness · review rank (sentiment model for 3-star reviews)
Random Forest
83.3% test
PyTorch MLP
68.7% test
The sentiment model is trained on the whole dataset, then used inside the single-drug study.

Cleaning the review text

Raw reviews are noisy, so each one passes through a four-step cleaning pipeline before vectorisation:

  1. Emoticons like :) and :( are replaced with the placeholder words happyemoticon and sademoticon, so their sentiment survives punctuation removal.
  2. Punctuation is replaced with spaces.
  3. Stopwords from scikit-learn and NLTK combined, plus tokens starting with a digit, are removed.
  4. Lemmatisation with spaCy (en_core_web_sm) lowercases each word and reduces it to its base form.
Drug review analysis (Colab)Source (opens GitHub in a new tab)
def clean_review(text, STOPWORDS=STOPWORDS, nlp=nlp):    """Cleans up review text for sentiment analysis"""    # …    def rem_stop_num(text):        """Remove stopwords and tokens starting with digits"""        return " ".join(word for word in text.split()                       if word not in STOPWORDS and not word[0].isdigit())     def lemmatize(text):        """Lemmatize, lowercase, and strip"""        return " ".join(tok.lemma_.lower().strip() for tok in nlp(text)                       if tok.lemma_ != "-PRON-")     # Pipeline    text = rep_emo(text)    text = rep_punct(text)    text = rem_stop_num(text)    text = lemmatize(text)     return text

A sentiment model from 320,000 reviews

After cleaning, 320,753 reviews remain. Each is labelled from its satisfaction rating: below 3 is negative, above 3 is positive, and exactly 3 is neutral. The data is split 75/25 with stratification, neutral reviews are set aside, and the binary task is learned from a TF-IDF representation (2,500 features, terms appearing in at least 10 reviews and at most 80% of them) with a Random Forest classifier.

ClassPrecisionRecallF1Test reviews
Negative (rating < 3)0.830.800.8232,110
Positive (rating > 3)0.840.860.8537,177

Overall test accuracy was 83.5% on 69,287 held-out reviews (99.2% on the training set). This model is used later to score the neutral, 3-star reviews, where the rating alone doesn't say which way the text leans.

Feature engineering

The main study uses the 6,524 lisinopril reviews left after removing empty and whitespace-only entries. From these the notebook builds:

  • Temporal features: year, month and day of the week, parsed from the review date.
  • Skew correction: UsefulCount and EaseofUse are scaled into [1, 2] and passed through a Box-Cox power transform, so heavily skewed counts don't dominate.
  • Encoding: one-hot columns for age group, sex, month and weekday, and a standardised effectiveness rating.
  • Review rank: for ratings below or above 3 it follows the rating, and for neutral 3-star reviews it comes from the sentiment model's prediction on the cleaned text.
  • Synthetic demographics: a name column generated with Mimesis and a race column sampled at random in fixed proportions. Because they're randomly generated, they exist only to exercise the pipeline and carry no real-world information.

The prediction target is a five-level side-effect score, defined in the notebook as the inverse of the satisfaction rating (satisfaction 5 maps to level 1, satisfaction 1 to level 5). The data is split 75/25, stratified by that score.

Random Forest vs MLP

Two models were trained on the same split. The Random Forest uses 100 trees on all engineered features. The MLP is a PyTorch network with three hidden layers (128, 64 and 32 units), each followed by batch normalisation, GELU and 40% dropout, with Kaiming initialisation. It trains for 400 full-batch epochs with AdamW and cross-entropy loss, using the four numeric features.

Drug review analysis (Colab)Source (opens GitHub in a new tab)
class PowerfulMLP(nn.Module):    def __init__(self, in_dim, out_dim, dropout=0.4):        super(PowerfulMLP, self).__init__()        self.fc1 = nn.Linear(in_dim, 128)        self.bn1 = nn.BatchNorm1d(128)        self.fc2 = nn.Linear(128, 64)        self.bn2 = nn.BatchNorm1d(64)        self.fc3 = nn.Linear(64, 32)        self.bn3 = nn.BatchNorm1d(32)        self.fc4 = nn.Linear(32, out_dim)        self.dropout = nn.Dropout(dropout)        self.act = nn.GELU()    # …    def forward(self, x):        x = self.act(self.bn1(self.fc1(x)))        x = self.dropout(x)        x = self.act(self.bn2(self.fc2(x)))        x = self.dropout(x)        x = self.act(self.bn3(self.fc3(x)))        x = self.dropout(x)        out = self.fc4(x)        return out

On the 1,631 test reviews the Random Forest reached 83.3% accuracy (99.8% on training data) and the MLP 68.7%. The per-level breakdown shows where the gap comes from: the MLP handles the extremes well but rarely predicts the middle levels 3 and 4.

F1 score by side-effect level (test set)

  • Random Forest
  • MLP
Random Forest · 0.89
MLP · 0.83
Random Forest · 0.82
MLP · 0.67
Random Forest · 0.69
MLP · 0.34
Random Forest · 0.72
MLP · 0.07
Random Forest · 0.89
MLP · 0.80
Show as a table
LevelRandom Forest F1MLP F1Test reviews
Level 10.890.83276
Level 20.820.67202
Level 30.690.34258
Level 40.720.07211
Level 50.890.80684
Level 1 corresponds to satisfaction 5, level 5 to satisfaction 1. Hover a bar for its value.

Takeaways

The full notebook, with every chart and output, is on Google Colab.

↑ ↓ to move · Enter to open