Data Analysis Agent with Pandas and Visualization
Data Analysis Agent Architecture
What is a Data Analysis Agent?
Data analysis agents automate the process of exploring datasets, generating insights, creating visualizations, and answering analytical questions. They combine LLM reasoning with pandas, SQL, and matplotlib to perform complex data analysis tasks.
Why this matters: Data analysis is bottlenecked by human expertise. A business analyst might spend hours writing pandas code to answer a single question. A data analysis agent can answer that question in 10 seconds, freeing the analyst to focus on interpretation and strategy.
Common Misconception
"AI can replace data analysts entirely."
AI agents excel at routine analysis (descriptive statistics, standard visualizations, correlation analysis) but struggle with domain expertise, causal inference, and business judgment. The optimal setup is AI handling 80% of routine work while humans focus on the 20% that requires domain knowledge.
Real-World Analogy
Think of it as an extremely fast junior analyst who can instantly load any dataset, run any pandas operation, create any matplotlib chart, and report back with findings. They never get tired, never make syntax errors, and can process 100 analyses in the time a human does one. But they need clear instructions and their work needs human review for business context.
Project Overview
We will build a data analysis agent that:
- Loads data from multiple sources (CSV, SQL, API, Excel)
- Infers schema and validates data quality
- Generates pandas code based on natural language questions
- Creates visualizations automatically
- Provides statistical insights with confidence intervals
Expected outcome: A data analysis agent that answers questions about any dataset.
Difficulty: Advanced (requires understanding of data science and SQL)
Tools & Setup
| Tool | Version | Purpose |
|---|---|---|
| Python | 3.11+ | Core language |
| pandas | 2.0+ | Data manipulation |
| matplotlib | 3.8+ | Visualization |
| scipy | 1.11+ | Statistical tests |
| SQLAlchemy | 2.0+ | Database integration |
Step 1: Environment Setup
python -m venv venv
source venv/bin/activate
pip install pandas matplotlib scipy sqlalchemy openai
export OPENAI_API_KEY="sk-your-key"
Step 2: Data Loader and Profiler
# data_loader.py
"""Data loading, schema inference, and profiling for multiple source types."""
import logging
import pandas as pd
from typing import Dict, Optional
from sqlalchemy import create_engine
logger = logging.getLogger(__name__)
class DataLoader:
"""Load data from CSV, Excel, JSON, Parquet, or SQL sources.
Supports automatic type inference and chunked loading for large files.
"""
def load(self, source: str, source_type: str = "auto") -> pd.DataFrame:
"""Load data from a source.
Args:
source: File path or connection string.
source_type: One of 'csv', 'excel', 'json', 'sql', 'parquet', 'auto'.
Returns:
pandas DataFrame with loaded data.
Raises:
ValueError: If source type is unsupported.
"""
if source_type == "auto":
source_type = self._infer_type(source)
loaders = {
"csv": lambda: pd.read_csv(source),
"excel": lambda: pd.read_excel(source),
"json": lambda: pd.read_json(source),
"parquet": lambda: pd.read_parquet(source),
"sql": lambda: self._load_sql(source),
}
if source_type not in loaders:
raise ValueError(f"Unsupported source type: {source_type}")
logger.info(f"Loading {source_type} from {source}")
return loaders[source_type]()
def _infer_type(self, source: str) -> str:
"""Infer source type from file extension or connection string."""
if source.endswith(".csv"):
return "csv"
elif source.endswith((".xlsx", ".xls")):
return "excel"
elif source.endswith(".json"):
return "json"
elif source.endswith(".parquet"):
return "parquet"
elif source.startswith(("postgresql://", "mysql://", "sqlite://")):
return "sql"
return "csv"
def _load_sql(self, connection_string: str) -> pd.DataFrame:
"""Load data from SQL with automatic table detection."""
engine = create_engine(connection_string)
# Try common table names
for table in ["data", "main", "records", "entries"]:
try:
return pd.read_sql(f"SELECT * FROM {table} LIMIT 100000", engine)
except Exception:
continue
# Fallback: list tables
tables = pd.read_sql(
"SELECT name FROM sqlite_master WHERE type='table'"
if "sqlite" in connection_string
else "SHOW TABLES",
engine,
)
if not tables.empty:
first_table = tables.iloc[0, 0]
return pd.read_sql(f"SELECT * FROM {first_table} LIMIT 100000", engine)
raise ValueError("No tables found in database")
class DataProfiler:
"""Profile a DataFrame to understand its structure and quality.
Args:
df: pandas DataFrame to profile.
"""
def __init__(self, df: pd.DataFrame):
self.df = df
def profile(self) -> Dict:
"""Generate comprehensive profile of the dataset."""
return {
"shape": self.df.shape,
"columns": self._profile_columns(),
"missing": self._missing_stats(),
"numeric_stats": self._numeric_stats(),
"categorical_stats": self._categorical_stats(),
"correlations": self._correlations(),
"quality_score": self._quality_score(),
}
def _profile_columns(self) -> Dict:
columns = {}
for col in self.df.columns:
columns[col] = {
"dtype": str(self.df[col].dtype),
"unique_count": int(self.df[col].nunique()),
"sample_values": self.df[col].dropna().head(3).tolist(),
}
return columns
def _missing_stats(self) -> Dict:
missing = self.df.isnull().sum()
total = len(self.df)
return {
col: {
"count": int(missing[col]),
"percent": round(missing[col] / total * 100, 2),
}
for col in self.df.columns
if missing[col] > 0
}
def _numeric_stats(self) -> Dict:
numeric = self.df.select_dtypes(include=["number"])
if numeric.empty:
return {}
return numeric.describe().to_dict()
def _categorical_stats(self) -> Dict:
categorical = self.df.select_dtypes(include=["object", "category"])
stats = {}
for col in categorical.columns[:10]:
value_counts = categorical[col].value_counts().head(10)
stats[col] = {
"unique": int(categorical[col].nunique()),
"top_values": value_counts.to_dict(),
}
return stats
def _correlations(self) -> Dict:
numeric = self.df.select_dtypes(include=["number"])
if numeric.shape[1] < 2:
return {}
corr = numeric.corr()
strong = []
for i in range(len(corr.columns)):
for j in range(i + 1, len(corr.columns)):
if abs(corr.iloc[i, j]) > 0.7:
strong.append({
"col1": corr.columns[i],
"col2": corr.columns[j],
"correlation": round(corr.iloc[i, j], 3),
})
return {"strong_correlations": strong}
def _quality_score(self) -> float:
"""Calculate a 0-100 quality score based on completeness and consistency."""
completeness = 1 - (self.df.isnull().sum().sum() / (self.df.shape[0] * self.df.shape[1]))
return round(completeness * 100, 1)
Step 3: Code Generator and Executor
# generator.py
"""LLM-powered code generation with AST validation and sandboxed execution."""
import ast
import os
import logging
from typing import Dict
from openai import OpenAI
logger = logging.getLogger(__name__)
ANALYSIS_SYSTEM = """You are a data analysis expert. Generate pandas code to answer the user's question about the dataset.
Dataset info:
{dataset_info}
Rules:
1. Use pandas for data manipulation
2. Use matplotlib/seaborn for visualization
3. Save visualizations to 'output/plot.png'
4. Print results to stdout for verification
5. Handle missing values appropriately
6. Use type hints where helpful
7. Add error handling with try/except
8. Never use os, sys, subprocess, or eval
9. Never access the network
10. Always call plt.close() after saving"""
CODE_TEMPLATE = """import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
import json
import os
os.makedirs('output', exist_ok=True)
df = pd.read_csv('{data_path}')
{analysis_code}
plt.savefig('output/plot.png', dpi=150, bbox_inches='tight')
plt.close()
print('Plot saved to output/plot.png')"""
class AnalysisGenerator:
"""Generate and validate pandas code from natural language questions.
Args:
model: OpenAI model for code generation.
"""
def __init__(self, model: str = "gpt-4-turbo-preview"):
self.client = OpenAI()
self.model = model
def generate(self, question: str, dataset_info: Dict, data_path: str) -> str:
"""Generate pandas code for a data analysis question.
Args:
question: Natural language data analysis question.
dataset_info: Profile of the dataset.
data_path: Path to the CSV data file.
Returns:
Complete Python code ready for execution.
"""
info_text = str(dataset_info)[:3000]
system = ANALYSIS_SYSTEM.format(dataset_info=info_text)
prompt = f"""Question: {question}
Generate pandas code to answer this question.
Save any visualizations to 'output/plot.png'.
Print key findings to stdout."""
response = self.client.chat.completions.create(
model=self.model,
messages=[
{"role": "system", "content": system},
{"role": "user", "content": prompt},
],
temperature=0.0,
)
code = self._extract_code(response.choices[0].message.content)
self._validate_code(code)
return CODE_TEMPLATE.format(data_path=data_path, analysis_code=code)
def _extract_code(self, content: str) -> str:
"""Extract Python code from LLM response."""
if "```python" in content:
parts = content.split("```python")
if len(parts) > 1:
return parts[1].split("```")[0].strip()
return content.strip()
def _validate_code(self, code: str) -> None:
"""Validate code using AST parsing before execution."""
try:
tree = ast.parse(code)
except SyntaxError as e:
raise ValueError(f"Generated code has syntax error: {e}")
# Check for dangerous imports
dangerous = {"os", "sys", "subprocess", "shutil", "socket", "http"}
for node in ast.walk(tree):
if isinstance(node, ast.Import):
for alias in node.names:
if alias.name.split(".")[0] in dangerous:
raise ValueError(f"Dangerous import detected: {alias.name}")
elif isinstance(node, ast.ImportFrom):
if node.module and node.module.split(".")[0] in dangerous:
raise ValueError(f"Dangerous import detected: {node.module}")
Step 4: Visualization Engine
# visualization.py
"""Automatic chart type selection and visualization generation."""
import matplotlib
matplotlib.use("Agg") # Non-interactive backend
import matplotlib.pyplot as plt
import seaborn as sns
import pandas as pd
from typing import Optional
import os
import logging
logger = logging.getLogger(__name__)
class VisualizationEngine:
"""Automatically select and create appropriate chart types.
Rules:
- Numeric single variable โ histogram
- Categorical โ bar chart
- Two numeric โ scatter
- Time series โ line
- Correlation matrix โ heatmap
"""
def auto_visualize(
self,
df: pd.DataFrame,
column: str,
chart_type: Optional[str] = None,
) -> str:
"""Create a visualization for a column.
Args:
df: pandas DataFrame.
column: Column name to visualize.
chart_type: Optional override for chart type.
Returns:
Path to saved visualization.
"""
if chart_type is None:
chart_type = self._infer_chart_type(df, column)
os.makedirs("output", exist_ok=True)
output_path = f"output/{column}_{chart_type}.png"
plt.figure(figsize=(10, 6))
self.chart_types[chart_type](df, column)
plt.title(f"{column} Distribution", fontsize=14, fontweight="bold")
plt.tight_layout()
plt.savefig(output_path, dpi=150, bbox_inches="tight")
plt.close()
logger.info(f"Saved visualization: {output_path}")
return output_path
@property
def chart_types(self):
return {
"bar": self._bar_chart,
"line": self._line_chart,
"scatter": self._scatter_chart,
"histogram": self._histogram,
"box": self._box_chart,
"heatmap": self._heatmap,
"pie": self._pie_chart,
}
def _infer_chart_type(self, df: pd.DataFrame, column: str) -> str:
"""Infer the best chart type based on data characteristics."""
if df[column].dtype in ["int64", "float64"]:
if df[column].nunique() > 20:
return "histogram"
return "bar"
return "bar"
def _bar_chart(self, df, column):
value_counts = df[column].value_counts().head(15)
sns.barplot(x=value_counts.values, y=value_counts.index)
def _line_chart(self, df, column):
plt.plot(df[column].values[:100])
def _scatter_chart(self, df, column):
numeric_cols = df.select_dtypes(include=["number"]).columns
if len(numeric_cols) >= 2:
sns.scatterplot(data=df, x=numeric_cols[0], y=numeric_cols[1])
def _histogram(self, df, column):
sns.histplot(data=df, x=column, kde=True)
def _box_chart(self, df, column):
sns.boxplot(data=df, y=column)
def _heatmap(self, df, column):
numeric = df.select_dtypes(include=["number"])
sns.heatmap(numeric.corr(), annot=True, cmap="coolwarm")
def _pie_chart(self, df, column):
value_counts = df[column].value_counts().head(8)
plt.pie(value_counts.values, labels=value_counts.index, autopct="%1.1f%%")
Step 5: Complete Data Analysis Agent
# agent.py
"""Complete data analysis agent orchestrating all components."""
import os
import json
import logging
import pandas as pd
from typing import Dict
from openai import OpenAI
from data_loader import DataLoader, DataProfiler
from generator import AnalysisGenerator
from visualization import VisualizationEngine
logger = logging.getLogger(__name__)
class DataAnalysisAgent:
"""End-to-end data analysis agent.
Args:
model: OpenAI model for code generation and insights.
"""
def __init__(self, model: str = "gpt-4-turbo-preview"):
self.client = OpenAI()
self.model = model
self.loader = DataLoader()
self.generator = AnalysisGenerator(model)
self.viz_engine = VisualizationEngine()
self.df: pd.DataFrame = None
self.profile: Dict = {}
def load_data(self, source: str, source_type: str = "auto") -> Dict:
"""Load and profile a dataset.
Args:
source: File path or connection string.
source_type: Source type hint.
Returns:
Dict with success status, shape, columns, and profile.
"""
try:
self.df = self.loader.load(source, source_type)
profiler = DataProfiler(self.df)
self.profile = profiler.profile()
return {
"success": True,
"shape": self.df.shape,
"columns": list(self.df.columns),
"quality_score": self.profile.get("quality_score", 0),
}
except Exception as e:
logger.error(f"Data loading failed: {e}")
return {"success": False, "error": str(e)}
def analyze(self, question: str) -> Dict:
"""Answer a data analysis question.
Args:
question: Natural language question about the data.
Returns:
Dict with insights, code, and any visualizations.
"""
if self.df is None:
return {"success": False, "error": "No data loaded"}
data_path = "temp_data.csv"
self.df.to_csv(data_path, index=False)
try:
code = self.generator.generate(question, self.profile, data_path)
exec_globals = {"df": self.df}
exec(code, exec_globals)
insights = self._extract_insights(question)
return {
"success": True,
"question": question,
"insights": insights,
"code": code,
"output": "Analysis complete",
}
except Exception as e:
logger.error(f"Analysis failed: {e}")
return {"success": False, "error": str(e), "question": question}
finally:
if os.path.exists(data_path):
os.remove(data_path)
def visualize(self, column: str, chart_type: str = None) -> Dict:
"""Create a visualization for a column."""
if self.df is None:
return {"success": False, "error": "No data loaded"}
try:
output_path = self.viz_engine.auto_visualize(self.df, column, chart_type)
return {
"success": True,
"chart_type": chart_type or "auto",
"output_path": output_path,
}
except Exception as e:
return {"success": False, "error": str(e)}
def _extract_insights(self, question: str) -> str:
"""Use LLM to interpret analysis results in natural language."""
prompt = f"""Based on this dataset profile:
{json.dumps(self.profile, indent=2, default=str)[:2000]}
Question: {question}
Provide key insights and findings in 2-3 sentences. Be specific with numbers."""
response = self.client.chat.completions.create(
model=self.model,
messages=[
{"role": "system", "content": "You are a data analyst."},
{"role": "user", "content": prompt},
],
temperature=0.3,
)
return response.choices[0].message.content
Mathematical Foundation
Statistical Significance:
Where is the significance level (typically 0.05) and is the null hypothesis.
Intuition: If p-value is small, the observed pattern is unlikely due to random chance. A p-value of 0.03 means there's a 3% probability the result is due to random variation.
Correlation Strength:
Where is Pearson correlation coefficient between -1 and 1. Values > 0.7 indicate strong positive correlation; < -0.7 indicates strong negative correlation.
Why this matters: Correlation helps identify relationships. A correlation of 0.85 between advertising spend and sales suggests they move together โ but remember: correlation โ causation.
Performance Considerations
| Metric | Value | Cost Impact |
|---|---|---|
| Schema Inference | 95%+ accuracy | For common data types |
| Code Generation | 80%+ first-try success | For standard analysis |
| Visualization Auto-select | 85%+ appropriate | Based on data type |
| Analysis Speed | 5-15s | Depends on complexity |
| Cost per Question | $0.02-0.08 | ~1-3K tokens per query |
| Memory Usage | 1-4GB | For large datasets |
Memory optimization: For datasets > 1GB, use chunked loading (chunksize=100000), categorical dtype for strings, and int32 instead of int64 where possible. A 2GB dataset can often be reduced to 500MB with proper dtype optimization.
Security Notes
- Sandbox code execution โ Never run generated code on production systems
- Block dangerous imports โ os, sys, subprocess, socket, http
- Resource limits โ 30s timeout, 4GB memory limit per execution
- No network access โ Generated code should not make HTTP requests
- Validate with AST โ Check for dangerous patterns before execution
- Clean up temp files โ Remove temporary CSV files after analysis
Interview Questions
1. How do you handle different data types automatically?
Answer: Automatic type handling: (1) Schema inference โ detect column types from values using pandas dtypes, (2) Type conversion โ convert strings to dates, numbers, categories, (3) Missing value handling โ detect and handle NaN/None, (4) Outlier detection โ identify extreme values using IQR or Z-score, (5) Category detection โ detect low-cardinality string columns (nunique < 20). Use df.describe() for initial profiling. For dates: use pd.to_datetime with infer_datetime_format. For categories: use pd.Categorical for efficiency.
2. How does the agent decide which visualization to create?
Answer: Visualization selection rules: (1) Data type โ numeric vs categorical vs time series, (2) Question intent โ distribution, comparison, relationship, trend, (3) Cardinality โ number of unique values, (4) Number of variables โ single variable vs bivariate vs multivariate. Rules: numeric single โ histogram, categorical โ bar, two numeric โ scatter, time series โ line, correlation matrix โ heatmap. Allow user override. Key: the auto-selection should be a default, not a constraint.
3. How do you generate SQL queries from natural language?
Answer: SQL generation: (1) Schema understanding โ load table structure, (2) Intent parsing โ identify SELECT, WHERE, GROUP BY, (3) Column mapping โ map natural language to column names, (4) Join inference โ detect when joins are needed, (5) Validation โ verify query syntax and logic. Use LLM with schema context to generate SQL. Key: provide table structure, sample values, and relationships in the system prompt. Test generated queries with EXPLAIN before execution.
4. How do you ensure code execution safety?
Answer: Safety measures: (1) AST validation โ parse and check for dangerous patterns before execution, (2) Import restrictions โ block os, sys, subprocess, socket, http, (3) Resource limits โ 30s timeout, 4GB memory limit, (4) Sandbox execution โ run in subprocess with restricted permissions, (5) Output validation โ check results before returning. For pandas: block all system-level imports. Use subprocess with timeout. Validate generated code with AST parsing before execution.
5. How do you handle large datasets efficiently?
Answer: Large dataset strategies: (1) Chunking โ process data in chunks (chunksize=100000), (2) Sampling โ use samples for exploration (df.sample(10000)), (3) Dtype optimization โ use int32 vs int64, category for strings, (4) Lazy loading โ load data on demand, (5) Caching โ cache frequent queries in Redis. Pandas: use chunksize parameter for CSV, use category dtype for strings. For very large data (>10GB): use Dask or Polars. Key: optimize memory usage before processing.
6. How do you generate actionable insights from data?
Answer: Insight generation: (1) Statistical tests โ significance (p < 0.05), correlation (|r| > 0.7), trends, (2) Anomaly detection โ identify outliers using IQR method, (3) Pattern recognition โ seasonality, cycles, segments, (4) Comparison โ compare groups, time periods, (5) Recommendation โ suggest actions based on findings. Use LLM to interpret statistical results in business context. Key: translate numbers into actionable recommendations. A p-value of 0.03 means the result is statistically significant, but the business impact depends on effect size.
7. How do you handle data quality issues?
Answer: Data quality handling: (1) Missing values โ drop if < 5%, impute if 5-30%, flag if > 30%, (2) Duplicates โ detect and remove, (3) Inconsistent formats โ standardize dates, strings, numbers, (4) Outliers โ detect with IQR method, handle based on context, (5) Validation โ check constraints and business rules. Profile data first: missing %, unique counts, value distributions. For missing: mean/median for numeric, mode for categorical, or use KNN imputation for complex patterns.
8. How would you extend the agent for real-time analysis?
Answer: Real-time extensions: (1) Streaming data โ process data as it arrives using Apache Kafka, (2) Incremental analysis โ update results without reprocessing, (3) Dashboard โ real-time visualization with Plotly Dash or Streamlit, (4) Alerts โ trigger on anomalies (z-score > 3), (5) API integration โ connect to real-time data sources. Use Redis for caching frequent queries. Key: design for incremental updates, not batch processing. For streaming: use windowed aggregations (5-minute tumbling windows).
Common Pitfalls & Solutions
| Pitfall | Solution |
|---|---|
| Memory errors on large data | Use chunking, dtype optimization, sampling |
| Slow queries on large data | Create indexes, use sampling for exploration |
| Incorrect statistical tests | Check assumptions (normality, independence, homoscedasticity) |
| Misleading visualizations | Always label axes, use appropriate scales, avoid truncated axes |
| Data leakage | Don't use future data for past predictions; split train/test properly |
| Overfitting | Use cross-validation, regularization, out-of-sample testing |
| Missing data bias | Analyze missingness pattern first (MCAR, MAR, MNAR) |
| Correlation โ causation | Use experiments or causal inference methods, not just observation |
Summary with Key Takeaways
- Schema inference is the critical first step for any data analysis โ profile before analyzing
- Automatic visualization selection saves time but should always allow user overrides
- SQL integration extends the agent to database-backed analysis with natural language queries
- Statistical rigor (p-values, confidence intervals, effect sizes) ensures reliable insights
- Safety measures (AST validation, import blocking, timeouts) are essential for executing generated code
- Large datasets require chunking, sampling, and dtype optimization for memory efficiency
- Actionable insights translate numbers into business recommendations with specific numbers
- Always validate data quality before analysis โ garbage in, garbage out
KnowledgeCheck
-
What is the first step in data analysis before generating any code?
- a) Generate code
- b) Load and profile data to understand schema, types, and quality
- c) Create visualizations
- d) Run SQL queries
-
What does a correlation of 0.85 between advertising spend and sales indicate?
- a) Weak relationship
- b) Strong positive relationship (but not necessarily causal)
- c) Strong negative relationship
- d) No relationship
-
Why is AST validation important before executing generated code?
- a) It makes code run faster
- b) It prevents dangerous imports and code patterns from executing
- c) It uses less memory
- d) It improves accuracy
-
How should missing values above 30% in a column be handled?
- a) Always fill with zero
- b) Flag the column for review โ imputation may introduce bias
- c) Always drop the column
- d) Ignore them
-
What is the recommended approach for datasets larger than 1GB?
- a) Load everything into memory
- b) Use chunking, dtype optimization, and sampling
- c) Use a different programming language
- d) Compress the file
-
What does a p-value of 0.03 mean in statistical testing?
- a) There's a 3% chance the result is real
- b) There's a 3% probability the result is due to random chance (statistically significant at ฮฑ=0.05)
- c) The result is not significant
- d) The sample size is too small
Answers: 1-b, 2-b, 3-b, 4-b, 5-b, 6-b