Алексей — финансовый директор. Умный, занятой, не любящий ждать. Каждый понедельник он открывает Excel с продажами за прошлую неделю и задаёт вопросы.

Но Excel — не собеседник. Алексей идёт к аналитику.

Аналитик строит сводную, ищет причины, пишет письмо. Иногда это занимает полдня. Иногда — до вторника.

Я посчитал: среднее время от вопроса Алексея до ответа было 2 часа 17 минут. Сейчас — 4 минуты 30 секунд. Алексей пишет вопрос в чат, получает ответ с цифрами и объяснением.

Расскажу, как это работает. Без BI-систем, без баз данных, без аналитика в цепочке — просто Python и Claude API.

Что за задача

Типичный файл финансового отдела: CSV или Excel с колонками — дата, категория, менеджер, сумма, регион. 3000–50 000 строк. Обновляется раз в день или раз в неделю.

Вопросы бывают двух видов.

Простые: «Какой итог за февраль по Москве?»

Сложные: «Почему в феврале минус 15% к январю — это сезон или конкретные менеджеры провалились?»

На простые Excel справляется. На сложные — нет: нужно копать, сравнивать, делать вывод. Вот здесь и нужен аналитик. Или — Claude.

Архитектура за 5 минут

Excel/CSV → pandas → Claude API → текстовый ответ с цифрами

Без векторных баз, без SQL, без инфраструктуры. Файл читается, данные готовятся для контекста, задаётся вопрос — получается ответ.

Полный код — ~75 строк.

Код

import anthropic
import pandas as pd
import json
from pathlib import Path


def get_table_summary(df: pd.DataFrame) -> dict:
    """Статистика по таблице — используется для больших файлов."""
    summary = {
        "shape": {"rows": df.shape[0], "cols": df.shape[1]},
        "columns": list(df.columns),
        "dtypes": df.dtypes.astype(str).to_dict(),
        "null_counts": df.isnull().sum().to_dict(),
        "numeric_stats": {},
    }
    for col in df.select_dtypes(include="number").columns:
        summary["numeric_stats"][col] = {
            "min": float(df[col].min()),
            "max": float(df[col].max()),
            "mean": round(float(df[col].mean()), 2),
            "sum": float(df[col].sum()),
        }
    return summary


def prepare_data_for_context(df: pd.DataFrame, max_rows: int = 200) -> str:
    """
    Маленькие таблицы — целиком.
    Большие — статистика + случайная выборка 50 строк.
    """
    if len(df) <= max_rows:
        return df.to_csv(index=False)

    summary = get_table_summary(df)
    sample = df.sample(min(50, len(df)), random_state=42)
    return (
        f"ТАБЛИЦА БОЛЬШАЯ ({len(df)} строк) — даю статистику и случайную выборку.\n\n"
        f"СТАТИСТИКА:\n{json.dumps(summary, ensure_ascii=False, indent=2, default=str)}\n\n"
        f"СЛУЧАЙНАЯ ВЫБОРКА (50 строк из {len(df)}):\n{sample.to_csv(index=False)}"
    )


def fix_column_types(df: pd.DataFrame) -> pd.DataFrame:
    """
    Фикс типов для выгрузок из 1С и других систем:
    пробелы как разделители тысяч, запятые вместо точек в числах.
    """
    for col in df.columns:
        col_lower = col.lower()
        if any(x in col_lower for x in ["дата", "date", "период", "месяц"]):
            try:
                df[col] = pd.to_datetime(df[col], dayfirst=True)
            except Exception:
                pass
        elif df[col].dtype == object:
            cleaned = (
                df[col]
                .astype(str)
                .str.replace(r"\s", "", regex=True)
                .str.replace(",", ".")
            )
            try:
                df[col] = pd.to_numeric(cleaned)
            except (ValueError, TypeError):
                pass
    return df


def ask_about_data(file_path: str, question: str) -> str:
    path = Path(file_path)
    if path.suffix.lower() == ".csv":
        df = pd.read_csv(path)
    else:
        df = pd.read_excel(path)

    df = fix_column_types(df)
    data_context = prepare_data_for_context(df)

    prompt = f"""Ты аналитик данных. Перед тобой данные из таблицы.

ДАННЫЕ:
{data_context}

ВОПРОС:
{question}

Правила:
- Отвечай конкретно, с цифрами из данных.
- Если данных не хватает для точного ответа — скажи об этом прямо.
- Не придумывай числа, которых нет в таблице.
- Если нужна разбивка — дай её."""

    client = anthropic.Anthropic()
    message = client.messages.create(
        model="claude-opus-4-6",
        max_tokens=2048,
        messages=[{"role": "user", "content": prompt}]
    )
    return message.content[0].text


if __name__ == "__main__":
    answer = ask_about_data(
        "sales_february_2026.xlsx",
        "Почему в феврале падение 15% к январю? Это сезон или конкретные менеджеры?"
    )
    print(answer)

Как выглядит диалог

Файл: выгрузка продаж за январь–февраль 2026, 4 200 строк, 7 менеджеров, 3 региона.

Вопрос Алексея: «Почему в феврале минус 15% к январю — это сезон или менеджеры?»

Ответ за 4 секунды:

Падение в феврале — 14,8% к январю (январь: 8 340 000 ₽, февраль: 7 104 000 ₽).

Разбивка по менеджерам:
— Иванова: −3% (в норме, сезонный паттерн)
— Петров: −8% (небольшое снижение)
— Сидоров: −41% (критическое падение: 1 820 000 → 1 070 000 ₽)
— Козлова: −2% (стабильно)
— Остальные: ±5% (в пределах нормы)

Вывод: 78% общего падения объясняется Сидоровым — у него провал именно
в Московском регионе (−52%). Остальные менеджеры держатся.
Это не сезон — это точечная проблема.

Рекомендую: уточнить у Сидорова по сделкам в Москве в феврале.

Алексей сразу идёт к Сидорову. Не ждёт аналитика, не строит сводную сам.

Где грабли

1. Галлюцинации в цифрах

Первую неделю я принимал все ответы за правду. На второй — заметил расхождение.

В одном анализе Claude написал сумму 1 234 500 ₽. В таблице стояло 1 243 500 ₽. Разница 9 000 — копейки в процентах, но неприятно. Добавил в промпт: «Каждую цифру сверяй с данными. Если не уверен — пиши "требует проверки".»

Стало лучше. Но не идеально — на больших выборках Claude иногда считает агрегаты с погрешностью ±1–2%. Для оперативного анализа ок, для бухгалтерии — нет.

2. Контекстное окно

Таблица на 50 000 строк в контекст не лезет. Поэтому prepare_data_for_context — она режет большие файлы до статистики + случайная выборка.

Минус очевидный: на случайной выборке можно пропустить аномалию. Если у вас 100 строк и одна из них — ключевой выброс, а она не попала в выборку — Claude её не увидит.

Решение на практике: для файлов > 5 000 строк я сначала прошу «дай общую статистику», потом «посмотри конкретно за февраль» — уже меньше данных, влезает целиком.

3. Форматы из 1С

Это отдельная боль. Выгрузки из 1С и многих российских систем выглядят так:

1 234 567,89  ← пробел как разделитель тысяч, запятая как десятичная

pandas читает это как строку. fix_column_types как раз это и фиксит — убирает пробелы, меняет запятую на точку.

Но бывают сюрпризы: в одной выгрузке даты шли как «Январь 2026», в другой — «01.2026», в третьей — числом 202601. Первые два pd.to_datetime ест, третий — нет.

Пришлось добавить отдельный обработчик для числовых дат.

4. Название колонок

Никто в мире не называет колонки одинаково. В одном файле «Сумма продаж», в другом «Выручка», в третьем «Revenue_RUB». Claude справляется с интерпретацией — это его сильная сторона. Но иногда путается, если колонок много и названия перекрываются.

Помогает: в начале диалога один раз спросить «Какие колонки есть в таблице и что они значат?» — получить описание, потом задавать вопросы.

Ограничения — честно

Не подходит для:

  • Таблиц > 50 000 строк без предобработки

  • Точной бухгалтерии (нужна проверка цифр)

  • Сложных join'ов между несколькими файлами (теоретически можно, но громоздко)

  • Real-time данных — придётся перечитывать файл каждый раз

Подходит для:

  • Оперативного анализа: «что происходит прямо сейчас»

  • Вопросов «почему» — когда нужен вывод, а не просто цифры

  • Нерегулярных запросов — тех, под которые не строят дашборды

  • Людей, которые не умеют в Excel pivot tables

Что дальше

Алексей теперь спрашивает сам. Аналитик занимается более сложными задачами.

Следующий шаг — веб-интерфейс: загрузи файл, задай вопрос, получи ответ. Без Python, без терминала. Это уже другая история.


А у вас есть такая задача — перевести данные из «немых» таблиц в диалог? Как решаете вопрос "переводчика" между данными и нужным человеком?


Сергей Цветков. AI-автоматизация бизнес-процессов. 15 лет в IT, 30+ AI-проектов.