Skip to content

MedVeritas

Predicting whether patients found a drug effective from what they wrote about it, across 366,815 reviews. Re-reading the pipeline for this write-up, I found it had been handed part of the answer, so I ran it again without the leak: a point lower overall, and twenty points lower for drugs with a single review.

Case studyUpdated October 15, 20265 min read

The Streamlit app's overview. Ratings pile up at 1 and 10: patients tend to love a drug or hate it.
Role
Pipeline, models and the Streamlit app; Ramzi Masri cleaned the DrugLib data. DATA@NEU project.
When
October – November 2025; re-run October 2026
Stack
Python, pandas, scikit-learn, XGBoost, NLTK, VADER, TextBlob, gensim (LDA), Streamlit

The problem

Patients write hundreds of words about what a drug did for them. Can that text predict whether they found it effective, and which words matter?

What I built

A pipeline over 366,815 reviews from three public datasets: cleaning, sentiment scores, TF-IDF, three classifiers and three regressors, topic modelling, and a Streamlit app to compare drugs and score a review.

What happened

Leak-free, XGBoost gets 77.6% accuracy and 0.85 ROC-AUC on 73,363 held-out reviews; the leaky version had claimed 78.6% and 0.86. For drugs with a single review, the leak had been worth twenty points.

  • 366,815

    reviews analysed, from 606,077 raw

  • 5,711

    drugs

  • 77.6%

    accuracy, leak-free (0.85 ROC-AUC)

  • 90 → 70%

    accuracy on single-review drugs, once the leak is closed

01

Three datasets, one table

The data is patient reviews from public datasets (Drugs.com and DrugLib.com among them), each with a drug, a condition, the review text and a rating from 1 to 10. Merged, that's 606,077 rows. Cleaning removed 239,262 of them, leaving 366,815 reviews of 5,711 drugs. Ramzi Masri cleaned the DrugLib portion.

I called a review “effective” if it rated the drug 7 or higher, which splits the data 60/40 (220,975 effective, 145,840 not). The ratings aren't spread evenly: they bunch at the ends, at 1 and at 10, condition after condition.

Ratings for the ten most-reviewed conditions. Look closely and “For Birth Control” and “Birth Control” are separate panels, as are “Depression” and “For Depression”; more on that below.

02

From reviews to features

Each review was cleaned and lemmatised, then scored for sentiment with both VADER and TextBlob (all 366,815 of them, in batches of 10,000). On top of that: the 500 most frequent words as TF-IDF features, the review's length, the year and month, the drug and condition, how many reviews each drug and condition has, and each drug's and each condition's average rating. That's 520 features in all.

The data was split 80/20, with 293,452 reviews to train on and 73,363 held out. I trained three classifiers (logistic regression, random forest, XGBoost) for effective or not, and three regressors for the exact rating, plus an LDA topic model to see what people actually write about.

03

What the models said

XGBoost won both tasks. Predicting the exact rating is much harder than effective-or-not: an R² of 0.43 means the text explains under half of the variation in ratings, which makes sense when the same words show up in five-star and one-star reviews.

The topic model found ten themes, and they're very human: side effects, sleep and anxiety; periods, cramps and bleeding for birth control; and the first days and weeks of taking something new.

Effective or notAccuracyF1ROC-AUC
Logistic regression73.5%0.790.80
Random forest76.1%0.820.83
XGBoost78.6%0.830.86

For the exact rating, XGBoost reached an RMSE of 2.63 and R² of 0.43, against 2.77 and 0.37 for linear regression. All on the 73,363 held-out reviews, and all optimistic, for the reason in the next chapter.

The most common words in positive (left) and negative (right) reviews. They're mostly the same words, which is why the text alone only gets you so far. The stray “_x000d_”, “quot” and “amp” are leftovers from the cleaning.

04

Re-reading it: the model was handed part of the answer

Two of those 520 features are each drug's and each condition's average rating. They were computed over all 366,815 reviews before the train/test split. So every review's own rating, held-out reviews included, went into the average the model used to predict that same rating. That's target leakage.

How much it matters depends on how many reviews a drug has. For a popular drug, one rating barely moves the average. For a drug with a handful of reviews, the average is close to the answer, and for a drug with one review, it is the answer. The app's own comparison page opens on two drugs with one review each, rated 1.0 and 10.0.

So the 78.6% and 0.86 were an upper bound, not a result. The fix is simple: compute those averages from the training reviews only, then run everything again. So I did.

For a drug with one review, its average rating is the answer.
The comparison tool's default view: two drugs with one review each, so their “effectiveness” is 0% and 100%. It needs a minimum number of reviews before it compares anything.

05

Running it again without the leak

I re-ran the repository's own pipeline end to end: the same 366,815 reviews, the same 80/20 split, the same models and settings. One thing changed. Each drug's and each condition's average rating now comes from the 293,452 training reviews only, and a drug the training set has never seen gets the overall training average. First I checked that the unchanged pipeline reproduces the original notebook, and it does, to within a twentieth of a point (78.63% against 78.58%).

Overall, the leak was worth about one point. XGBoost goes from 78.6% to 77.6% accuracy and from 0.86 to 0.85 ROC-AUC; random forest from 76.1% to 75.4%; logistic regression from 73.5% to 72.9%. That's because most held-out reviews are of popular drugs, where one review barely moves an average. Break the results down by how many reviews a drug has, and the picture changes. There are 911 drugs with a single review, and on them accuracy falls from 90.5% to 70.0%.

The averages are still worth having once they're honest. Dropped entirely, XGBoost reaches 76.4%; computed from training reviews only, 77.6%. A drug's track record is real signal. It just has to come from reviews the model isn't being tested on.

Overall, the leak was worth a point. For a drug with one review, it was worth twenty.
Drug's reviews in the dataHeld-out reviewsWith the leakLeak-free
119090.5%70.0%
2–580287.2%72.8%
6–509,58580.3%77.4%
51–50030,78278.0%77.4%
501+32,00478.5%77.9%

XGBoost accuracy on the 73,363 held-out reviews. For the exact rating, R² goes from 0.43 to 0.41. Run on current library versions, with two adjustments to match the original environment: the one missing condition is kept as its own category, and absent TF-IDF entries are zeros, as they were under pandas 2.

06

What the cleaning missed

Writing this up meant looking at every figure again, and they show three more things I'd fix:

  • Merging datasets merged their labelling conventions. One source writes “For Birth Control” and another “Birth Control”, so the same condition is counted twice, which helps explain how there are 7,798 “conditions”.
  • Text artefacts survived into the features: “_x000d_” is an Excel line-break code, and “quot” and “amp” are HTML entities. They're common enough to sit in both word clouds, so they very likely made the TF-IDF vocabulary too.
  • The app compares averages with no minimum. A drug with one review gets the same big headline numbers as one with ten thousand.

What I'd carry forward

  1. 1Anything computed from the label belongs inside the training split, or it isn't a feature. It's the answer.
  2. 2Break results down by group. An overall number moved by a point and hid a twenty-point gap.
  3. 3Look at the word cloud before you trust it. It's the quickest audit of your text cleaning there is.
  4. 4Merging datasets merges their labelling conventions too.
  5. 5An average needs its sample size next to it.

Get in touch

Questions about my work, a role, or a project idea. Send a message here or email me directly.

Srikarmks@gmail.com