Analyze reviews with AI: from CSV to insights

Load a CSV of reviews, a cheap model labels each one, pandas aggregates the stats, a second request writes the report. The full path from data to insights.

3 min · · development · бизнес

Reading hundreds of reviews by hand takes a long time, and what you really need is the gist: what customers like, what annoys them, and which problems keep coming up. Below is a full review-analysis pipeline via API: a cheap model labels each review (sentiment, topics, quote), pandas computes the stats, and a pricier model writes the final report from the aggregated data.

Overall approach

A two-stage approach saves money:

  1. Labeling (cheap model): for each review — sentiment (positive/neutral/negative), topics from a fixed list, the main point, and a characteristic quote.
  2. Report (pricier model): one request with stats and sample quotes, producing conclusions and recommendations.

Most tokens are spent in the first stage, so it uses glm-5.3-flash ($0.04/$0.04 per 1M tokens). The report is a single request, so it can go to claude-sonnet-5 ($0.30/$0.30) at a negligible price difference.

Step 1: load the CSV

bash
import csv
import json
from collections import Counter

import pandas as pd
from openai import OpenAI

client = OpenAI(
    base_url="https://api.zukko.pro/v1",
    api_key="fetch_your_key",
)

def load_reviews(csv_path: str) -> list[dict]:
    """Expected columns: id, review_text, rating, date"""

Sample CSV:

id,review_text,rating,date 1,"Great product, arrived fast",5,2024-01-15 2,"Terrible quality, the seams split after a week",1,2024-01-16 3,"Fine, but expensive",3,2024-01-17

Step 2: label each review

Topics come from a fixed list — otherwise the model will name the same thing differently ("shipping", "courier", "logistics"), and the stats fall apart.

bash
TOPICS = ["quality", "price", "shipping", "packaging", "size", "service", "product description", "other"]

ANALYSIS_PROMPT = f"""Analyze a customer review.
Return ONLY JSON, no markdown:
{{"sentiment": "positive|neutral|negative", "score": 0.5, "topics": ["..."], "main_issue": "...", "key_quote": "..."}}

Rules:
- score — sentiment from -1 (very negative) to 1 (enthusiastic)
- topics — 1–3 topics only from this list: {', '.join(TOPICS)}
- main_issue — one sentence: the main problem or the main strength
- key_quote — an exact quote from the review, no longer than 15 words
"""

def parse_json(text: str):

Reviews marked unknown (the model didn't return JSON) are worth re-running or checking by eye.

Step 3: aggregate in pandas

bash
def aggregate(analyzed: list[dict]) -> dict:
    df = pd.DataFrame(analyzed)
    sentiment = df["sentiment"].value_counts()
    topics = Counter(t for ts in df["topics"] for t in ts)
    negative = df[df["sentiment"] == "negative"].sort_values("score")
    positive = df[df["sentiment"] == "positive"].sort_values("score", ascending=False)
    df.to_csv("analyzed_reviews.csv", index=False)
    return {
        "total": len(df),
        "sentiment": sentiment.to_dict(),
        "top_topics": topics.most_common(5),
        "negative_quotes": negative["key_quote"].head(5).tolist(),
        "positive_quotes": positive["key_quote"].head(5).tolist(),
        "negative_by_topic": Counter(t for ts in negative["topics"] for t in ts).most_common(5),
    }

A useful slice is topics in negative reviews specifically (negative_by_topic): it shows what most often causes dissatisfaction, not just what people write about most.

Step 4: the summary report

The model receives only the stats and a few quotes, not the whole review set — a short request:

bash
def generate_report(stats: dict, model: str = "claude-sonnet-5") -> str:
    total = stats["total"] or 1
    lines = [f"- {k}: {v} ({v / total:.0%})" for k, v in stats["sentiment"].items()]
    prompt = f"""Below are stats for {stats['total']} customer reviews.

Sentiment:
{chr(10).join(lines)}

Common topics: {stats['top_topics']}
Topics in negative reviews: {stats['negative_by_topic']}

Quotes from negative reviews:
{chr(10).join('- ' + q for q in stats['negative_quotes'])}

Running the whole thing

bash
def run_pipeline(csv_path: str, report_path: str = "review_report.txt"):
    reviews = load_reviews(csv_path)
    print(f"Loaded {len(reviews)} reviews")
    stats = aggregate(analyze_all(reviews))
    report = generate_report(stats)
    with open(report_path, "w", encoding="utf-8") as f:
        f.write(report)
    print(f"Report saved to {report_path}")
    return stats, report

if __name__ == "__main__":
    run_pipeline("reviews.csv")

For thousands of reviews, send first-stage requests in parallel with AsyncOpenAI, capping the number of concurrent requests and retrying on a 429 error.

Cost

Estimate for 1,000 reviews:

Stage 1: glm-5.3-flash ($0.04/$0.04 per 1M) ~300 input and ~100 output tokens per review (1000 × 300 × 0.04 / 1M) + (1000 × 100 × 0.04 / 1M) = $0.012 + $0.004 = $0.016 Stage 2: claude-sonnet-5 ($0.30/$0.30 per 1M), one request ~2000 input and ~800 output tokens (2000 × 0.30 / 1M) + (800 × 0.30 / 1M) = $0.0006 + $0.00024 ≈ $0.0008 Total: ≈ $0.017 per 1,000 reviews

If you label with claude-sonnet-5 instead, stage 1 costs 1000 × 400 × 0.30 / 1M = $0.12. Whether the difference is worth it will show up when you check a sample (see the last FAQ question).

Current models and prices: GET https://api.zukko.pro/v1/models or the Models page in your dashboard.

Set up the key

Create a key in the Zukko dashboard and allow both models — glm-5.3-flash and claude-sonnet-5. A $1 monthly limit covers tens of thousands of reviews by the estimate above. See also the quick start and Python examples.

FAQ

What if reviews are in different languages?

Add to the prompt: "the review may be in any language; pick topics from the English list and keep the quote in the original language". Check labeling quality on a few reviews in each language.

How often should I update the report?

It depends on volume. With dozens of reviews a day, a weekly report on new reviews compared with the previous week works well.

Can I split the report by product category?

Yes. Add a category column to the CSV, group the labeled reviews in pandas, and call generate_report for each group separately.

What if the model mislabels reviews?

Manually check a random sample of 50–100 reviews. If errors are systematic, refine the prompt (for example, topic definitions) or compare another model on the same sample.

zukko

Pay 25×
less

for Claude Code & ChatGPT

One fetch_* key and base URL api.zukko.pro — for Cursor, Claude Code, Codex, and any SDK.

Get API keyDocumentation

api.zukko.pro · fetch_*

Review analysis with AI via API: Python example · Zukko