title: "AI агенты для анализа данных: pandas-ai, Code Interpreter и генерация SQL" slug: agents-data-analysis-pandas-code-interpreter-2026-ru date: 2026-02-21 lang: ru tags: [data-analysis, pandas, code-interpreter, sql-generation, llm-agents, e2b] description: "Создание production AI-агентов для анализа данных: запросы к DataFrame в естественном языке с pandas-ai, безопасное выполнение кода с E2B, NL-to-SQL с контекстом схемы и конвейеры генерации графиков."
AI агенты для анализа данных: pandas-ai, Code Interpreter и генерация SQL
Ключевые факты
- pandas 3.0.1 (выпущена 2025-12, последняя стабильная версия); крупные breaking changes от 2.x включают copy-on-write по умолчанию
- e2b-code-interpreter 2.4.1 (последняя версия); предоставляет изолированные облачные песочницы для выполнения кода; каждая песочница работает в свежем контейнере
- langchain-experimental 0.4.1 включает обёртку
create_pandas_dataframe_agent(); по умолчанию используетPythonREPLTool(локальное выполнение) - matplotlib 3.10.8 (последняя версия) и plotly 6.5.2 для генерации графиков; E2B поддерживает обе через backend
matplotlib.use('Agg') - LangChain PythonREPLTool выполняет Python код локально используя
exec()с ограниченными globals; опасно для сгенерированного LLM кода (может получить доступ к файловой системе, сети, subprocess) - E2B sandbox security: изолированные Docker контейнеры с 2GB RAM, 30GB хранилища; не могут получить доступ к host-системе, production базам данных или учётным данным
- create_pandas_dataframe_agent() сигнатура функции:
create_pandas_dataframe_agent(llm, df, agent_type="openai-tools", verbose=False, allow_dangerous_code=False) - Установка
allow_dangerous_code=Trueвключает локальное выполнение черезexec(); требуется для LangChain агентов но небезопасно без sandboxing - Overhead токенов для DataFrames: DataFrame с 1000 строк × 10 столбцов с типичными string/numeric данными занимает ~3000-8000 токенов при отправке
df.head(5).to_markdown()в LLM; полныйdf.to_string()может превышать 100K токенов для средних датасетов - E2B free tier: 100 часов выполнения sandbox/месяц; каждое invocation sandbox считает время от создания до завершения (типично 5-30 секунд на query)
- За пределами 100 часов: $0.01 за минуту выполнения (~$0.60/час); биллится ежемесячно с автоматическим shutdown неиспользуемых sandbox после 5 минут
- Chart output из E2B: графики сохраняются через
plt.savefig('output.png')и получаются как base64-закодированные PNG черезexecution.results[0].png; типичный график 1200×600 это 50-200KB - Для отрисовки графиков в ответе: декодировать base64 → записать в файл → встроить в markdown как
или сохранить в static path - pandas-agent iteration: LLM генерирует код → E2B выполняет → при ошибке LLM получает traceback → генерирует новый код; типично сходится за 1-3 итерации
- Schema context optimization: отправлять имена столбцов + dtypes + примеры значений (3-5 строк) вместо полного DataFrame; снижает использование токенов на 90%+
- SQL validation: использовать библиотеку
sqlparseдля парсинга сгенерированного SQL и блокировки INSERT/UPDATE/DELETE/DROP до выполнения - Common failure mode: код pandas сгенерированный LLM предполагает наличие столбцов без проверки; использовать schema introspection перед генерацией кода чтобы включить доступные столбцы в prompt
- Modal alternative: Modal.com предоставляет похожий sandboxing с поддержкой GPU; требует написания custom executor class заменяющего E2B API calls
Что такое Data Analysis Agent
Data analysis agent работает на основе loop генерации кода + выполнения: LLM пишет Python или SQL код для ответа на естественно-языковой вопрос пользователя, выполняет его в безопасной среде, наблюдает результат (или ошибку) и итерирует до успеха.
В отличие от прямых LLM ответов (которые должны быть правильными с первой попытки), паттерн code interpreter позволяет агенту:
- Написать exploratory код для понимания формы данных
- Увидеть ошибки выполнения и исправить их
- Уточнить вычисления на основе промежуточных результатов
- Генерировать графики и валидировать visual output
Эта multi-step refinement резко улучшает точность для сложных queries включающих multi-table joins, date arithmetic или статистические расчёты.
Chart generation расширяет loop: LLM решает тип графика (line/bar/scatter/heatmap), генерирует matplotlib/seaborn код с правильными labels и стилизацией, выполняет его в sandbox и возвращает PNG bytes. Агент может переделать попытку с разными типами графиков если первая попытка не даёт ясного ответа на вопрос.
Decision Framework
| Подход | Безопасность | Стоимость | Latency | Use Case |
|---|---|---|---|---|
| PythonREPLTool (локальный) | Опасно; код LLM работает на вашем сервере с доступом к файловой системе/сети | Бесплатно | ~200ms на выполнение | Только разработка; никогда в production |
| E2B sandbox | Изолированный контейнер; нет доступа к production системам | $0.01/мин после 100ч/месяц free tier | ~2-5s на выполнение (включает startup контейнера) | Production default для большинства use cases |
| Modal | Изолированный; поддерживает GPU для ML workloads | $0.0003/сек CPU, $0.003/сек GPU | ~3-8s cold start, <1s warm | Large-scale ML inference или heavy compute |
Security tradeoff: локальное выполнение в 10-25× быстрее но позволяет LLM коду потенциально читать environment variables, получить доступ к базам через connection strings или делать outbound HTTP requests. E2B добавляет latency но гарантирует изоляцию.
Cost tradeoff: при 20 queries/day в среднем 10 секунд каждая, E2B стоит ~$1/месяц (хорошо в free tier). При 500 queries/day стоимость растёт до ~$25/месяц.
Recommendation: используйте E2B для production. Только используйте локальное выполнение в разработке с dummy данными, никогда с реальными credentials в environment variables.
Parameters Reference Table
| Параметр | Значение | Примечания |
|---|---|---|
model |
claude-opus-4-6 |
Best для сложной pandas/SQL генерации; Claude Sonnet приемлемо для простых queries |
max_tokens |
2048 | Генерация кода; 512 для интерпретации результатов |
timeout_seconds (E2B) |
60 | Timeout выполнения; увеличить до 120 для больших DataFrames (>1M строк) |
max_iterations |
5 | Retry limit генерации кода; 3 достаточно для 90% queries |
allow_dangerous_code (LangChain) |
False |
Должно быть True чтобы включить agent, но только используйте с E2B sandboxing |
cache_results |
True |
Кешировать question → answer mapping; invalidate когда DataFrame меняется |
df.head() rows |
5 | Строки для показа как sample в schema context; 3-5 оптимально для token efficiency |
result_limit (SQL) |
10000 | Max строк возвращаемых из сгенерированных queries; предотвращает memory overflow |
rate_limit |
20 requests/min | Per-user limit чтобы предотвратить quota exhaustion |
Common Pitfalls
❌ Pitfall 1: Отправка полного DataFrame в LLM context
Impact: DataFrame с 10,000 строк может потребить 50K+ токенов, превышая context limits или вызывая медленные ответы.
# ❌ BAD: Отправляет весь DataFrame representation
messages = [{
"role": "user",
"content": f"Answer this question:\n{df.to_string()}\n\nQuestion: {question}"
}]
# ✅ GOOD: Отправляет только schema + sample
schema = {
"shape": df.shape,
"columns": [
{
"name": col,
"dtype": str(df[col].dtype),
"sample": df[col].head(3).tolist()
}
for col in df.columns
]
}
messages = [{
"role": "user",
"content": f"Schema:\n{json.dumps(schema)}\n\nQuestion: {question}"
}]
❌ Pitfall 2: Использование allow_dangerous_code=True без sandboxing
Impact: LLM-сгенерированный код может читать /etc/passwd, получить доступ к AWS credentials из environment или exfiltrate данные.
# ❌ DANGEROUS: Выполняется на host системе
agent = create_pandas_dataframe_agent(
llm, df, allow_dangerous_code=True
)
agent.run("What is the average revenue?") # LLM код работает с полным доступом к host
# ✅ SAFE: Выполнить в E2B sandbox
executor = E2BSafeExecutor(api_key=os.environ['E2B_API_KEY'])
code = llm_generate_code(question, schema)
result = executor.execute_with_dataframe(code, df) # Работает в изолированном контейнере
❌ Pitfall 3: Не валидировать SQL перед выполнением
Impact: LLM может генерировать DROP TABLE или UPDATE statements, корруптируя production данные.
# ❌ BAD: Выполняет сгенерированный SQL вслепую
sql = llm.generate_sql(question)
df = pd.read_sql(sql, db_connection) # Может быть "DROP TABLE users;"
# ✅ GOOD: Валидировать что SQL read-only
import sqlparse
def validate_sql_safe(sql: str) -> bool:
parsed = sqlparse.parse(sql)
for stmt in parsed:
if stmt.get_type() not in ['SELECT', 'UNKNOWN']:
return False
dangerous = ['INSERT', 'UPDATE', 'DELETE', 'DROP', 'ALTER', 'TRUNCATE']
return not any(kw in sql.upper() for kw in dangerous)
sql = llm.generate_sql(question)
if not validate_sql_safe(sql):
raise SecurityError("Generated SQL contains mutation operations")
df = pd.read_sql(sql, db_connection)
❌ Pitfall 4: Отсутствие проверок существования столбцов в prompts
Impact: LLM генерирует код вроде df['revenue'].sum() когда столбец на самом деле называется total_revenue, вызывая KeyError.
# ❌ BAD: Prompt не включает доступные столбцы
prompt = f"Write pandas code to calculate average revenue for this dataset."
# ✅ GOOD: Включить точные имена столбцов в prompt
columns = df.columns.tolist()
prompt = f"""Write pandas code to calculate average revenue.
Available columns: {columns}
Rules:
- Check column exists before using it
- Use exact column names from the list above
"""
❌ Pitfall 5: Отсутствие retry logic для ошибок выполнения
Impact: Одна синтаксическая ошибка или неправильная ссылка на столбец вызывает полный отказ.
# ❌ BAD: One-shot выполнение
code = llm.generate_code(question, schema)
result = executor.execute(code)
if not result.success:
return {"error": result.error} # Сдаёмся сразу
# ✅ GOOD: Retry с error feedback
code = llm.generate_code(question, schema)
result = executor.execute(code)
if not result.success:
# Отправить ошибку обратно в LLM на исправление
fixed_code = llm.fix_code(question, code, result.error, schema)
result = executor.execute(fixed_code)
return result
Pandas Agent Implementation
create_pandas_dataframe_agent из LangChain оборачивает DataFrame с conversational interface. Агент использует PythonREPLTool чтобы выполнять код локально по умолчанию.
from langchain_experimental.agents import create_pandas_dataframe_agent
from langchain_anthropic import ChatAnthropic
import pandas as pd
llm = ChatAnthropic(model="claude-opus-4-6")
df = pd.read_csv("sales_data.csv")
# WARNING: allow_dangerous_code=True выполняет LLM код на host
# Используйте только в разработке или с E2B sandboxing
agent = create_pandas_dataframe_agent(
llm,
df,
agent_type="openai-tools",
verbose=True,
allow_dangerous_code=True # Required but dangerous
)
response = agent.invoke("What is the total revenue by product category?")
print(response)
Prompt tuning для лучшей генерации кода:
prefix = """You are a data analysis expert working with a pandas DataFrame.
Rules for code generation:
1. Always check if columns exist before using them
2. Handle NaN values explicitly (use .dropna() or .fillna())
3. For date columns, parse with pd.to_datetime() first
4. Round numeric results to 2 decimal places
5. Limit output to top 20 rows for lists
6. Store final result in variable named `result`
Available DataFrame: `df`
Columns: {columns}
Shape: {shape}
"""
agent = create_pandas_dataframe_agent(
llm,
df,
agent_type="openai-tools",
prefix=prefix.format(
columns=df.columns.tolist(),
shape=df.shape
),
verbose=True,
allow_dangerous_code=True
)
Multi-DataFrame analysis:
sales_df = pd.read_csv("sales.csv")
customers_df = pd.read_csv("customers.csv")
# LangChain agent accepts list of DataFrames
agent = create_pandas_dataframe_agent(
llm,
[sales_df, customers_df],
allow_dangerous_code=True
)
# Agent can reference both as df1 and df2
response = agent.invoke(
"Join sales and customer data to find revenue by customer segment"
)
Агент будет генерировать код вроде:
merged = df1.merge(df2, left_on='customer_id', right_on='id')
result = merged.groupby('segment')['revenue'].sum()
E2B Code Interpreter Integration
E2B предоставляет secure sandboxes для выполнения LLM-сгенерированного кода без риска для вашей production окружения.
from e2b_code_interpreter import Sandbox
import pandas as pd
import base64
class E2BSafeExecutor:
def __init__(self, api_key: str, timeout: int = 60):
self.api_key = api_key
self.timeout = timeout
def execute_with_dataframe(self, code: str, df: pd.DataFrame) -> dict:
"""Execute code in isolated sandbox with DataFrame pre-loaded."""
# Serialize DataFrame to parquet (efficient binary format)
df_bytes = df.to_parquet(index=False)
df_b64 = base64.b64encode(df_bytes).decode()
# Wrap user code with DataFrame loading logic
full_code = f"""
import pandas as pd
import numpy as np
import matplotlib
matplotlib.use('Agg') # Non-interactive backend
import matplotlib.pyplot as plt
import base64
import io
# Load DataFrame from base64 parquet
df_bytes = base64.b64decode('{df_b64}')
df = pd.read_parquet(io.BytesIO(df_bytes))
# User-generated code
{code}
# Capture result
if 'result' in dir():
print("RESULT:", repr(result))
"""
with Sandbox(api_key=self.api_key, timeout=self.timeout) as sandbox:
execution = sandbox.run_code(full_code)
# Extract charts if generated
charts = []
for output in execution.results:
if hasattr(output, 'png') and output.png:
charts.append(base64.b64decode(output.png))
# Parse result from stdout
result = None
for log in execution.logs.stdout:
if log.startswith("RESULT:"):
result = eval(log[7:].strip())
if execution.error:
return {
"success": False,
"output": None,
"error": str(execution.error.value),
"charts": []
}
return {
"success": True,
"output": result,
"error": None,
"charts": charts
}
File upload для CSV анализа:
with Sandbox(api_key=api_key) as sandbox:
# Upload CSV to sandbox
sandbox.filesystem.write("data.csv", csv_content)
code = """
import pandas as pd
df = pd.read_csv('data.csv')
result = df.groupby('category')['sales'].sum().to_dict()
"""
execution = sandbox.run_code(code)
Chart retrieval и отрисовка:
code = """
import matplotlib.pyplot as plt
import pandas as pd
# Generate chart
df.groupby('month')['revenue'].sum().plot(kind='bar')
plt.title('Revenue by Month')
plt.xlabel('Month')
plt.ylabel('Revenue ($)')
plt.tight_layout()
plt.savefig('output.png', dpi=150, bbox_inches='tight')
result = 'chart generated'
"""
result = executor.execute_with_dataframe(code, df)
if result['charts']:
chart_bytes = result['charts'][0]
# Option 1: Save to file
with open('chart.png', 'wb') as f:
f.write(chart_bytes)
# Option 2: Base64 embed in markdown/HTML
chart_b64 = base64.b64encode(chart_bytes).decode()
html = f'<img src="data:image/png;base64,{chart_b64}" />'
SQL + Pandas Hybrid
Для больших датасетов хранящихся в базах, гибридный подход сначала запрашивает базу, затем загружает результаты в pandas для сложного анализа.
from anthropic import Anthropic
import pandas as pd
import sqlparse
class SQLPandasAgent:
def __init__(self, db_connection, e2b_executor):
self.db = db_connection
self.executor = e2b_executor
self.client = Anthropic()
def analyze(self, question: str, schema_context: dict) -> dict:
"""
1. Generate SQL to extract relevant data
2. Load results into pandas DataFrame
3. Use pandas agent for detailed analysis
"""
# Step 1: Generate SQL
sql = self._generate_sql(question, schema_context)
# Validate SQL is read-only
if not self._is_safe_sql(sql):
return {"error": "Generated SQL contains unsafe operations"}
# Step 2: Execute SQL
try:
df = pd.read_sql(sql, self.db)
except Exception as e:
# Retry with fixed SQL
sql = self._fix_sql(question, sql, str(e), schema_context)
df = pd.read_sql(sql, self.db)
# Step 3: Pandas analysis on result set
pandas_code = self._generate_pandas_analysis(question, df)
result = self.executor.execute_with_dataframe(pandas_code, df)
return {
"sql": sql,
"row_count": len(df),
"analysis_code": pandas_code,
"result": result['output'],
"success": result['success']
}
def _generate_sql(self, question: str, schema: dict) -> str:
response = self.client.messages.create(
model="claude-opus-4-6",
max_tokens=2048,
messages=[{
"role": "user",
"content": f"""Generate SQL to extract data for this question.
Database schema:
{json.dumps(schema, indent=2)}
Question: {question}
Rules:
- Use SELECT only (no INSERT/UPDATE/DELETE/DROP)
- Add LIMIT 100000 to prevent memory issues
- Use proper JOINs for related tables
- Include only columns needed for analysis
Return only the SQL query."""
}]
)
return response.content[0].text.strip()
def _is_safe_sql(self, sql: str) -> bool:
parsed = sqlparse.parse(sql)
for stmt in parsed:
if stmt.get_type() not in ['SELECT', 'UNKNOWN']:
return False
dangerous_keywords = ['INSERT', 'UPDATE', 'DELETE', 'DROP', 'ALTER', 'TRUNCATE']
return not any(kw in sql.upper() for kw in dangerous_keywords)
def _generate_pandas_analysis(self, question: str, df: pd.DataFrame) -> str:
schema_info = {
"columns": [
{"name": col, "dtype": str(df[col].dtype)}
for col in df.columns
],
"shape": df.shape
}
response = self.client.messages.create(
model="claude-opus-4-6",
max_tokens=2048,
messages=[{
"role": "user",
"content": f"""Write pandas code to analyze this data.
DataFrame schema (from SQL query results):
{json.dumps(schema_info, indent=2)}
Original question: {question}
The data has already been filtered by SQL. Now perform detailed analysis.
Store final answer in variable `result`.
Return only Python code."""
}]
)
return self._extract_code(response.content[0].text)
def _extract_code(self, text: str) -> str:
if '```python' in text:
return text.split('```python')[1].split('```')[0].strip()
elif '```' in text:
return text.split('```')[1].split('```')[0].strip()
return text.strip()
Usage example:
import psycopg2
db = psycopg2.connect("postgresql://user:pass@localhost/db")
executor = E2BSafeExecutor(api_key=os.environ['E2B_API_KEY'])
agent = SQLPandasAgent(db, executor)
schema_context = {
"orders": {
"columns": ["id", "customer_id", "order_date", "total_amount", "status"],
"description": "Customer orders with amounts and dates"
},
"customers": {
"columns": ["id", "name", "segment", "signup_date"],
"description": "Customer master data"
}
}
result = agent.analyze(
"What is the month-over-month revenue growth for enterprise customers?",
schema_context
)
print(f"SQL executed: {result['sql']}")
print(f"Rows retrieved: {result['row_count']}")
print(f"Analysis result: {result['result']}")
Агент будет:
- Генерировать SQL:
SELECT DATE_TRUNC('month', order_date) as month, SUM(total_amount) FROM orders JOIN customers ON orders.customer_id = customers.id WHERE segment = 'enterprise' GROUP BY month - Загружать 12 месяцев агрегированных данных в pandas (маленький DataFrame)
- Генерировать pandas код:
result = df.sort_values('month').pct_change()['sum'].iloc[-1] - Выполнять в E2B sandbox и возвращать процент роста
Performance & Benchmarks
Примечание: Приведённые ниже цифры являются иллюстративными оценками на основе типичных production-конфигураций, а не измерениями конкретной системы.
Query latency breakdown (E2B sandbox, Claude Opus 4):
- LLM код генерация: 1.5-3 секунды
- E2B sandbox startup: 0.8-2 секунды (cold start), 0.2-0.5 секунды (warm)
- Выполнение кода: 0.1-5 секунд (зависит от размера DataFrame и операций)
- Chart генерация: 0.5-2 секунды (matplotlib рендеринг)
- Total end-to-end: 3-12 секунд для типичных queries
Token consumption (Claude Opus 4):
- Schema context (50-column DataFrame): 800-1500 токенов
- Сгенерированный pandas код: 200-600 токенов
- Error traceback + retry: +300-500 токенов за итерацию
- Result интерпретация: 100-300 токенов
- Total на query: 1400-2900 токенов (без retries)
Cost estimates (Claude Opus 4 на $15/MTok input, $75/MTok output):
- Simple query (1 итерация, без графика): $0.03-0.05
- Complex query (2 итерации, с графиком): $0.08-0.15
- При 100 queries/день: $3-15/день ($90-450/месяц)
E2B sandbox efficiency:
- Cold start overhead: 40-60% всего latency для простых queries
- Warm sandbox reuse: снижает latency на 1-2 секунды
- Parallel execution: E2B поддерживает до 10 concurrent sandboxes на account
- Memory limit: 2GB за sandbox (достаточно для DataFrames до ~500K строк с 50 столбцами)
DataFrame size limits:
- Оптимальная performance: <100MB DataFrame (~500K строк × 20 столбцов смешанных типов)
- Приемлемая performance: 100-500MB (1-2M строк; требует увеличенного E2B timeout до 120s)
- Требует chunking: >500MB (использовать SQL фильтрацию чтобы снизить размер датасета перед pandas анализом)
Accuracy benchmarks (качественно):
- Single-step агрегации (sum, mean, count): высокая точность с ясным schema context
- Multi-step трансформации (pivot, merge, window functions): требует 1-2 retry итераций для сложных случаев
- Date arithmetic и timezone handling: частый источник ошибок; включить date format в schema context
- Chart type selection: LLM выбирает подходящий тип графика для большинства queries; иногда требует явной инструкции ("use a line chart")
Optimization strategies:
- Кешировать schema introspection результаты (имена столбцов, dtypes, примеры значений) чтобы избежать перевычисления
- Переиспользовать warm E2B sandboxes для sequential queries на том же датасете
- Pre-filter большие датасеты с SQL перед загрузкой в pandas
- Использовать parquet сериализацию вместо CSV для DataFrame трансфера (3-5× меньше, быстрее десериализации)
- Ограничить
df.head()samples до 3-5 строк в schema context (больше 5 строк добавляет токены без улучшения точности)