Анализ отзывов нейросетью: от CSV к выводам на Python

Загружаете CSV с отзывами, дешёвая модель размечает каждый, pandas считает статистику, второй запрос пишет отчёт. Полный цикл от данных к выводам.

3 мин · · разработка · бизнес

Читать сотни отзывов вручную долго, а понять нужно главное: что нравится клиентам, что раздражает и какие проблемы повторяются. Ниже — полный цикл анализа отзывов через API: дешёвая модель размечает каждый отзыв (тональность, темы, цитата), pandas считает статистику, а итоговый отчёт пишет модель подороже по уже агрегированным данным.

Общая схема

Двухэтапный подход экономит деньги:

  1. Разметка (дешёвая модель): для каждого отзыва — тональность (positive/neutral/negative), темы из фиксированного списка, главная мысль и характерная цитата.
  2. Отчёт (модель подороже): один запрос со статистикой и примерами цитат, на выходе — выводы и рекомендации.

Основной объём токенов приходится на первый этап, поэтому там берём glm-5.3-flash ($0,04/$0,04 за 1M токенов). Отчёт — один запрос, его можно отдать claude-sonnet-5 ($0,30/$0,30), разница в цене копеечная.

Шаг 1: загрузка 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_ваш_ключ",
)

def load_reviews(csv_path: str) -> list[dict]:
    """Ожидаемые колонки: id, review_text, rating, date"""

Пример CSV:

id,review_text,rating,date 1,"Отличный товар, пришёл быстро",5,2024-01-15 2,"Ужасное качество, швы разошлись через неделю",1,2024-01-16 3,"Нормально, но дорого",3,2024-01-17

Шаг 2: разметка каждого отзыва

Темы задаём фиксированным списком — иначе модель будет называть одно и то же по-разному («доставка», «курьер», «логистика»), и статистика развалится.

bash
TOPICS = ["качество", "цена", "доставка", "упаковка", "размер", "обслуживание", "описание товара", "другое"]

ANALYSIS_PROMPT = f"""Проанализируй отзыв покупателя.
Верни ТОЛЬКО JSON без markdown:
{{"sentiment": "positive|neutral|negative", "score": 0.5, "topics": ["..."], "main_issue": "...", "key_quote": "..."}}

Правила:
- score — тональность от -1 (резко негативно) до 1 (восторженно)
- topics — 1–3 темы только из списка: {', '.join(TOPICS)}
- main_issue — одно предложение: главная проблема или главное достоинство
- key_quote — точная цитата из отзыва, не длиннее 15 слов
"""

def parse_json(text: str):

Отзывы со статусом unknown (модель вернула не JSON) стоит пересчитать повторно или посмотреть глазами.

Шаг 3: агрегация в 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),
    }

Полезный срез — темы именно в негативных отзывах (negative_by_topic): он показывает, что чаще всего вызывает недовольство, а не просто о чём чаще пишут.

Шаг 4: итоговый отчёт

В модель уходит не весь массив отзывов, а только статистика и несколько цитат — это короткий запрос:

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"""Ниже статистика по {stats['total']} отзывам покупателей.

Тональность:
{chr(10).join(lines)}

Частые темы: {stats['top_topics']}
Темы в негативных отзывах: {stats['negative_by_topic']}

Цитаты из негативных отзывов:
{chr(10).join('- ' + q for q in stats['negative_quotes'])}

Полный запуск

bash
def run_pipeline(csv_path: str, report_path: str = "review_report.txt"):
    reviews = load_reviews(csv_path)
    print(f"Загружено {len(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_path}")
    return stats, report

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

Для тысяч отзывов запросы первого этапа удобно отправлять параллельно через AsyncOpenAI с ограничением числа одновременных запросов и повтором при ошибке 429.

Стоимость

Расчёт на 1000 отзывов:

Этап 1: glm-5.3-flash ($0.04/$0.04 за 1M) ~300 входных и ~100 выходных токенов на отзыв (1000 × 300 × 0.04 / 1M) + (1000 × 100 × 0.04 / 1M) = $0.012 + $0.004 = $0.016 Этап 2: claude-sonnet-5 ($0.30/$0.30 за 1M), один запрос ~2000 входных и ~800 выходных токенов (2000 × 0.30 / 1M) + (800 × 0.30 / 1M) = $0.0006 + $0.00024 ≈ $0.0008 Итого: ≈ $0.017 на 1000 отзывов

Если разметку делать на claude-sonnet-5, первый этап обойдётся в 1000 × 400 × 0.30 / 1M = $0.12. Стоит ли разница того, покажет сверка на выборке (см. последний вопрос ниже).

Актуальный список моделей и цен: GET https://api.zukko.pro/v1/models или страница «Модели» в кабинете.

Настройка ключа

Создайте ключ в кабинете Zukko и разрешите ему обе модели — glm-5.3-flash и claude-sonnet-5. Месячного лимита в $1 хватит на десятки тысяч отзывов по расчёту выше. См. также быстрый старт и примеры на Python.

Частые вопросы

Что если отзывы на разных языках?

Добавьте в промпт: «отзыв может быть на любом языке; темы выбирай из списка на русском, цитату приводи в оригинале». Проверьте качество разметки на нескольких отзывах каждого языка.

Как часто обновлять отчёт?

Зависит от потока. При десятках отзывов в день удобно собирать отчёт раз в неделю по новым отзывам и сравнивать с предыдущей неделей.

Можно ли разделить отчёт по категориям товаров?

Да. Добавьте колонку категории в CSV, сгруппируйте размеченные отзывы в pandas и вызовите generate_report для каждой группы отдельно.

Что если модель ошибается в разметке?

Проверьте вручную случайную выборку из 50–100 отзывов. Если ошибки систематические, уточните промпт (например, определение тем) или сравните на той же выборке другую модель.

zukko

Платите в 25×
меньше

за Claude Code & ChatGPT

Один ключ fetch_* и base URL api.zukko.pro — в Cursor, Claude Code, Codex и любом SDK.