Use Cases & Solutions

Analyze Customer Reviews with a Local LLM: Sentiment, Product Aspects and Top Complaints per Product

6 steps 19 min read 16 views 0
On this page

Products collect hundreds of reviews across Shopee, Lazada, Amazon, TikTok Shop and your own store, often in several languages. Star ratings show that something is wrong but not what. This guide uses a local language model to read every review and record its overall sentiment, the product aspects it praises or criticises, and the concrete problems it mentions. A report per product then shows the most criticised aspects, reviews whose rating does not match the text, and an AI summary of the main problems with suggested fixes. Everything runs on your own server, so customer texts are not sent to a third party.

Collect reviews only in ways the platform allows, such as seller-centre exports, official APIs or your own shop's database. Scraping marketplaces may break their terms. Remove personal data you do not need before the analysis.

Step 1: Install the Tools

apt install -y python3-venv
python3 -m venv /opt/reviews
/opt/reviews/bin/pip install ollama numpy scikit-learn
ollama pull qwen2.5:7b

Ollama setup is covered in running a local LLM with Ollama. numpy and scikit-learn are not needed for this guide, but the topic analysis in the companion guide uses them.

Step 2: Prepare the Reviews

Export your reviews to a UTF-8 CSV file. The columns id and text are required; sku, rating (1 to 5) and date are optional but make the report more useful:

id,sku,rating,date,text
R1001,LS-01,2,2026-09-14,"The stand wobbles when I type, screws came loose after a week."
R1002,LS-01,5,2026-09-15,"Sturdy, looks great on my desk, easy to fold."
R1003,BT-07,1,2026-09-15,"左耳机三天就没声音了,客服也没回复。"

Step 3: Create the Analysis Script

cat > /opt/reviews/reviews.py <<'EOF2'
import csv, json, os, sys
from collections import Counter, defaultdict
import ollama

MODEL = os.environ.get("MODEL", "qwen2.5:7b")
LANGUAGE = os.environ.get("REPORT_LANG", "English")       # language of issues, summaries and the report
ASPECTS = ["quality", "size_fit", "function", "durability", "ease_of_use",
           "delivery", "packaging", "price_value", "customer_service", "other"]
SENT = {"type": "string", "enum": ["positive", "neutral", "negative"]}
SCHEMA = {
    "type": "object",
    "properties": {
        "sentiment": SENT,
        "aspects": {"type": "array", "items": {"type": "object", "properties": {
            "aspect": {"type": "string", "enum": ASPECTS}, "sentiment": SENT}, "required": ["aspect", "sentiment"]}},
        "issues": {"type": "array", "items": {"type": "string"}},
        "summary": {"type": "string"},
    },
    "required": ["sentiment", "aspects", "issues", "summary"],
}
SYSTEM = ("You analyse customer reviews for an online shop. The review can be in any language. "
          "Give the overall sentiment, and the sentiment of each product aspect the review actually mentions. "
          f"List concrete problems as short phrases in {LANGUAGE} (empty list if there are none) "
          f"and write a one-sentence summary in {LANGUAGE}. Do not invent anything that is not in the review.")

def ask(system, text, schema=None):
    r = ollama.chat(model=MODEL, format=schema, options={"temperature": 0}, messages=[
        {"role": "system", "content": system}, {"role": "user", "content": text}])
    return r.message.content

src, out = sys.argv[1], "reviews_tagged.csv"
done = {}
if os.path.exists(out):
    with open(out, encoding="utf-8-sig") as f:
        done = {r["id"]: r for r in csv.DictReader(f)}
with open(src, encoding="utf-8-sig") as f:
    reviews = [r for r in csv.DictReader(f) if r.get("text", "").strip()]

fields = ["id", "sku", "rating", "sentiment", "aspects", "issues", "summary", "text"]
new_file = not os.path.exists(out)
with open(out, "a", newline="", encoding="utf-8-sig") as f:
    w = csv.DictWriter(f, fieldnames=fields)
    if new_file:
        w.writeheader()
    for r in reviews:
        if r["id"] in done:
            continue
        d = json.loads(ask(SYSTEM, r["text"][:4000], SCHEMA))
        row = {"id": r["id"], "sku": r.get("sku", ""), "rating": r.get("rating", ""), "sentiment": d["sentiment"],
               "aspects": "; ".join(f"{a['aspect']}:{a['sentiment']}" for a in d["aspects"]),
               "issues": "; ".join(i.strip() for i in d["issues"] if i.strip()), "summary": d["summary"], "text": r["text"]}
        w.writerow(row)
        f.flush()
        done[r["id"]] = row
        print(r["id"], d["sentiment"], flush=True)

by_sku = defaultdict(list)
for row in done.values():
    by_sku[row["sku"] or "-"].append(row)
with open("report.md", "w", encoding="utf-8") as f:
    for sku, rows in sorted(by_sku.items(), key=lambda x: -len(x[1])):
        sent = Counter(r["sentiment"] for r in rows)
        neg = Counter(a.split(":")[0] for r in rows for a in r["aspects"].split("; ") if a.endswith(":negative"))
        issues = [i for r in rows for i in r["issues"].split("; ") if i]
        odd = [r["id"] for r in rows if r["rating"].isdigit() and ((int(r["rating"]) >= 4 and r["sentiment"] == "negative")
               or (int(r["rating"]) <= 2 and r["sentiment"] == "positive"))]
        f.write(f"## {sku}: {len(rows)} reviews\n\n")
        f.write(f"- Positive {sent['positive']}, neutral {sent['neutral']}, negative {sent['negative']}\n")
        f.write("- Most criticised aspects: " + (", ".join(f"{a} ({n})" for a, n in neg.most_common(5)) or "none") + "\n")
        f.write("- Rating does not match the text: " + (", ".join(odd) or "none") + "\n\n")
        if issues:
            f.write(ask(f"You are a product manager. Group these customer complaints into the main problems, "
                        f"most frequent first, and suggest one concrete fix for each. Answer in {LANGUAGE}, briefly.",
                        "\n".join(issues[:300])) + "\n\n")
        print("report:", sku, flush=True)
print("done: reviews_tagged.csv, report.md")
EOF2

The JSON schema forces the model to answer in a fixed structure: one of three sentiments, aspects from your list, and a list of issues. Finished reviews are saved immediately, and a second run skips them, so you can stop and resume at any time or add new reviews to the same CSV later.

Step 4: Run the Analysis

cd /data/reviews
REPORT_LANG="Simplified Chinese" nohup /opt/reviews/bin/python /opt/reviews/reviews.py reviews.csv > reviews.log 2>&1 &
tail -f reviews.log

On a CPU server, a 7B model needs a few seconds to several tens of seconds per review, depending on its length; run large exports overnight or use a smaller model such as qwen2.5:3b with MODEL=qwen2.5:3b. The results are reviews_tagged.csv (one row per review) and report.md (one section per product).

Step 5: Read the Results

  • Most criticised aspects show where a product disappoints: durability, size and fit, delivery or packaging problems need different fixes and different teams.
  • Rating does not match the text lists, for example, five-star reviews that still complain, often about delivery, and one-star reviews that are actually positive. Both are worth a look.
  • The AI summary groups the complaints into main problems with a suggested fix for each; forward it to product development or your supplier.
  • Filter reviews_tagged.csv by product and negative sentiment to read the original reviews behind each problem.

Adapt the ASPECTS list to your category, for example fabric, size_fit and colour for clothing, or battery, connectivity and sound for electronics.

Step 6: Make It a Routine

Export new reviews every month, run the script on the same file, and compare the reports over time. To discover problems you did not expect, group the reviews into topics as described in finding what customers talk about with embeddings. To answer reviews faster, see drafting replies to customer reviews with AI. What customers praise most is also good material for your product listings.

FAQ

How accurate is the analysis?

For clear reviews, sentiment and aspects are reliable. Sarcasm, mixed reviews and very short texts such as "ok" are harder. Check 20 random reviews against the results before you rely on the numbers, and keep the temperature at 0 for consistent answers.

Can I analyse support tickets or chats the same way?

Yes. Put each ticket or conversation in the text column and replace the aspects with your support categories, such as billing, delivery, setup or account.

Can I use a cloud AI model instead?

Many AI APIs support structured output, but the reviews then leave your server. Check your privacy obligations, the provider's terms and its supported regions first.

Still stuck after following these steps? Open a support ticket and the IMIDC 24/7 technical team will help. Include the server IP, the model you use, the first lines of your CSV without personal data and the error output so we can pinpoint the issue faster.

Was this answer helpful?

Related Tutorials