Case study · consumer insights
What makes drinkware buyers unhappy, and can an AI sort the reviews?
I took {chk['rows']:,} Amazon reviews of insulated drinkware from four brands ({', '.join(BRANDS)}) and asked two questions: what do unhappy buyers write about, and can an AI read reviews well enough to watch them every month? An open AI model read every review, an LLM (Claude) named the main complaint in the {R['unhappy_total']} unhappy ones, and I checked the AI against the reviewers' own stars before using any of it.
What I found
- YETI's unhappy reviews split three ways, and one is easy to fix. {NAMES[top3[0][1]]} ({pct(top3[0][0])}), {NAMES[top3[1][1]].lower()} ({pct(top3[1][0])}) and {NAMES[top3[2][1]].lower()} ({pct(top3[2][0])}). The missing parts stand out: {lid['n']} of those {lid['of']} reviews say a lid piece was missing, {lid['slider']} of them the MagSlider.
- Each brand has its own weak spot. {'; '.join(f"{b['brand']}: {top_complaints(b)}" for b in others)}.
- The AI is good enough to read every review. It read the mood the way the stars say {pct(mood['accuracy'], 1)} of the time (kappa {mood['kappa']:.2f}), and after a one-number fix its confidence means what it says, so a cut-off can decide which reviews a person reads.
What YETI could do with it
- Find out where the lid pieces go missing. Packing, returned units sold again as new, or third-party sellers: each has a different fix, and the review dates and listings help tell them apart.
- Watch the temperature complaints. Keeping drinks cold or hot is the core promise, and it is {pct(yeti['temperature'])} of YETI's unhappy reviews. A monthly view shows whether that share moves after a product or lid change.
- Let the AI read every review, every month. At a 90% cut-off it sorts {pct(pick['auto_share'])} on its own and is right {pct(pick['auto_right'], 1)} of the time; a person reads the other {pct(1 - pick['auto_share'])}, the hard cases.
The main finding
What unhappy buyers write about, by brand
Each one- and two-star review got one main complaint, named by the LLM with a short codebook. Each row adds up to 100%.
{heat_table()}Reading behind YETI's missing parts
I read the {lid['of']} YETI reviews about a wrong or missing part. {lid['n']} say a lid piece was missing, and {lid['slider']} of those name the slider (the MagSlider). The other {lid['of'] - lid['n']}: a straw lid that didn't fit, a handle that didn't match the listing, and items that seemed used or fake. So the fix sits with packing, returns and sellers more than with product design.
The data
{chk['rows']:,} real reviews, checked before use
Source: Amazon Reviews 2023 (McAuley Lab, UC San Diego), the newest public release of Amazon reviews. I kept insulated tumblers, mugs and bottles from the four brands, the newest 21 months in the release ({chk['first_day']} to {chk['last_day']}), and drew 500 reviews per brand at random. A title filter dropped coolers and most accessories; a few lids and accessories sold on their own (mostly Hydro Flask's press-in lid) got through and are kept.
Limits: Amazon reviewers are not all buyers, and the release ends in September 2023. The method is the point: the same steps run on a brand's own reviews or survey comments.
How the AI read the reviews
A fast model for every review, an LLM for the complaints
Step 1. Laya is an open "decision model" (Apache 2.0, 421M parameters, runs on a laptop). Instead of writing text, it answers typed questions with a probability for each answer, so there is nothing to parse and nothing it can make up. Each review got these questions:
For 1-2 star reviews, seven yes/no questions: does the reviewer complain that it leaks or the lid is a problem · it doesn't keep drinks cold or hot · it arrived damaged or broke · a part was wrong or missing · the size, weight or design · cleaning, smell or taste · the price?
Step 2. Used as is, the decision model is very good at mood but mixes up complaint types: it agreed with the LLM on only {pct(R['coders_agree'])} of the unhappy reviews. So the {R['unhappy_total']} unhappy reviews also went to Claude with a one-paragraph codebook: the same complaint types and the rule "pick the one the reviewer stresses most". The complaint numbers on this page come from Claude.
Check 1 · Does it read the mood the way the reviewer rated it?
The stars are the reviewer's own verdict: 4-5 stars = happy, 1-2 = unhappy (3 stars are mixed and left out).
| Model: happy | Model: unhappy | |
|---|---|---|
| Stars: happy (4-5) | {mood['table']['stars_happy_ai_happy']:,} | {mood['table']['stars_happy_ai_unhappy']:,} |
| Stars: unhappy (1-2) | {mood['table']['stars_unhappy_ai_happy']:,} | {mood['table']['stars_unhappy_ai_unhappy']:,} |
Check 2 · When it says "90% sure", is it right 90% of the time?
I grouped the reviews by how sure the model said it was and checked how often it was right. Then I fixed it with one number (a "temperature", T = {cal['T']}), learned on one half of the reviews and tested on the other half.
The decision · Which reviews can it handle alone?
| Cut-off (model at least this sure) | Handled by the model | Right when it handled them | Sent to a person |
|---|---|---|---|
| {pct(r['cut'])} | {pct(r['auto_share'])} | {pct(r['auto_right'], 1)} | {r['to_person']} of {cal['test_n']} |
Next
What I'd set up to run every month
- A review tracker. Every new review scored the same way: mood, complaint type, model confidence. One table by product and month.
- A check set. 100 reviews labeled by a person, re-run whenever the model or the questions change, so a drop in agreement shows up before the numbers move. It also measures how far to trust the complaint types.
- A routing rule with a budget. The cut-off set by how many reviews a person can read each week.
- Alerts. When one complaint type jumps for a product (for example leaks after a lid change), flag it to the product team with five example reviews.
How it's built (code, files, limits)
fetch_data.py streams the public review files (41 GB) and draws the sample · run_model.py asks the decision model the questions and saves its answers (ai_answers.csv) · llm_code.py has Claude name the main complaint of each unhappy review (llm_complaints.csv) · analysis.py runs the checks and the brand comparison and writes results.json · build_page.py builds this page from results.json. All files: code.
m["ai_happy"] = (m["p_happy"] >= 0.5).astype(int) accuracy = (m["ai_happy"] == m["happy_by_stars"]).mean() p_fixed = p**(1/T) / (p**(1/T) + (1-p)**(1/T)) # the one-number fix (temperature)
Limits: one marketplace and one period; Amazon reviews skew to strong opinions; the complaint types are one LLM's reading and still need a person's check on a labeled sample (the check set above); the decision model was used as is, with no training on this data.