Skip to content
ExponaBLOG
PRACTICAL AI & MACHINE LEARNING · DAY 067 / 100

AI report generation in Practical AI & Machine Learning

Turn raw metrics into readable summaries.

Expona Admin8 min read

Most teams generate reports by throwing raw data at a language model and hoping for readable output. That approach is slow, expensive, and often produces hallucinations or inconsistent formatting. AI report generation in Practical AI & Machine Learning flips the script: compute your metrics in application code, let the model focus on writing natural narrative, template your structure, and have the AI surface what actually changed.

Compute numbers in code, not in the model

The foundation of any reliable report is accuracy. Language models are creative, but they're not calculators. If you ask Claude or GPT to compute a month-over-month revenue change from a dataset, it might get it right 95% of the time—and you'll never know which 5% is wrong until a stakeholder asks questions.

Your application code is deterministic. PostgreSQL gives you the same sum every time. Redis transactions don't hallucinate. Use these tools for all numeric work: aggregations, deltas, percentiles, running totals, comparisons. Then pass clean numbers to the model.

Consider a weekly metrics report for an e-commerce platform. Instead of sending the model raw transaction logs, compute:

  • Total revenue (database query)
  • Orders placed (count)
  • Average order value (sum / count)
  • Week-over-week change (this week vs. last week)
  • Top 3 product categories by revenue
  • Customer acquisition cost (ad spend / new customers)

All of this lives in application code—Python, Node.js, C#, whatever your stack is. Python example:

python
import psycopg2
from datetime import datetime, timedelta

def compute_weekly_metrics(db_conn, end_date=None):
    if end_date is None:
        end_date = datetime.now().date()
    
    start_date = end_date - timedelta(days=7)
    prev_start = start_date - timedelta(days=7)
    prev_end = start_date - timedelta(days=1)
    
    cursor = db_conn.cursor()
    
    # Current week
    cursor.execute("""
        SELECT 
            SUM(amount) as revenue,
            COUNT(*) as order_count,
            AVG(amount) as avg_order_value
        FROM orders
        WHERE created_at::date >= %s AND created_at::date < %s
    """, (start_date, end_date))
    
    current = cursor.fetchone()
    
    # Previous week
    cursor.execute("""
        SELECT 
            SUM(amount) as revenue,
            COUNT(*) as order_count
        FROM orders
        WHERE created_at::date >= %s AND created_at::date < %s
    """, (prev_start, prev_end))
    
    previous = cursor.fetchone()
    
    # Compute deltas
    revenue_delta = current[0] - previous[0] if current[0] and previous[0] else 0
    revenue_pct = (revenue_delta / previous[0] * 100) if previous[0] else 0
    
    return {
        "period": f"{start_date} to {end_date}",
        "revenue": round(float(current[0]) or 0, 2),
        "revenue_delta": round(float(revenue_delta), 2),
        "revenue_pct_change": round(float(revenue_pct), 1),
        "orders": int(current[1]) or 0,
        "avg_order_value": round(float(current[2]) or 0, 2),
    }

metrics = compute_weekly_metrics(conn)
print(metrics)
# Output: {'period': '2025-01-06 to 2025-01-13', 'revenue': 24500.00, ...}

Now your model receives a clean dictionary or JSON object. No guessing. No math errors.

Let the model write the narrative

Once numbers are locked in, language models excel at one thing: turning structure into prose. They write naturally, adapt tone, and explain context in ways that resonate with readers.

Feed your computed metrics into a prompt designed for narrative generation, not calculation. The model's job is to tell the story of what those numbers mean.

python
import anthropic

def generate_report_narrative(metrics, previous_context=None):
    client = anthropic.Anthropic()
    
    prompt = f"""You are a business analyst writing a weekly performance report.
    
Metrics for this week:
- Revenue: ${metrics['revenue']:,.2f}
- Change from last week: ${metrics['revenue_delta']:,.2f} ({metrics['revenue_pct_change']:+.1f}%)
- Orders: {metrics['orders']}
- Average order value: ${metrics['avg_order_value']:.2f}
- Report period: {metrics['period']}

Write 2-3 sentences summarizing this week's performance. Focus on:
1. Overall trend (up, down, flat)
2. Most significant change
3. Business implications

Keep it professional and concise. Assume the reader is a product manager or finance lead."""
    
    message = client.messages.create(
        model="claude-3-5-sonnet-20241022",
        max_tokens=300,
        messages=[
            {"role": "user", "content": prompt}
        ]
    )
    
    return message.content[0].text

narrative = generate_report_narrative(metrics)
print(narrative)

The model generates the narrative; you handle the logic. This separation lets you swap models, change prompts, or add fallback logic without rewriting metric computation.

Template the structure

Reports need consistent structure. Headers, sections, footers, tables. A template keeps output predictable and makes it easy to parse, style, or validate before sending to stakeholders.

Define your report structure as a schema or class. Then populate it systematically:

python
from dataclasses import dataclass
from typing import List, Dict

@dataclass
class ReportSection:
    title: str
    content: str
    metric_highlight: Dict[str, str] = None

@dataclass
class Report:
    title: str
    period: str
    executive_summary: str
    sections: List[ReportSection]
    footer: str
    generated_at: str

def build_report(metrics, narrative):
    report = Report(
        title="Weekly Performance Report",
        period=metrics['period'],
        executive_summary=narrative,
        sections=[
            ReportSection(
                title="Revenue Performance",
                content=f"Revenue reached ${metrics['revenue']:,.2f} this week.",
                metric_highlight={
                    "Current": f"${metrics['revenue']:,.2f}",
                    "Change": f"{metrics['revenue_pct_change']:+.1f}%"
                }
            ),
            ReportSection(
                title="Order Metrics",
                content=f"We processed {metrics['orders']} orders with an average value of ${metrics['avg_order_value']:.2f}.",
                metric_highlight={
                    "Total Orders": str(metrics['orders']),
                    "Avg Order Value": f"${metrics['avg_order_value']:.2f}"
                }
            )
        ],
        footer="This report was generated automatically. For questions, contact the analytics team.",
        generated_at=datetime.now().isoformat()
    )
    return report

report = build_report(metrics, narrative)
print(report.title)

Structure first, content second. This keeps reports scannable and lets you add validation, styling, or routing logic consistently.

Highlight changes and anomalies

A report full of numbers is noise. A report that highlights what changed or what's unusual is actionable.

Compute anomalies at the metric layer, not the narrative layer. Calculate standard deviations, flag values outside thresholds, compare to historical benchmarks. Then tell the model which metrics to emphasize.

python
import statistics

def detect_anomalies(metrics, historical_data):
    """Flag metrics that deviate significantly from baseline."""
    anomalies = []
    
    revenue_history = [m['revenue'] for m in historical_data[-12:]]  # Last 12 weeks
    mean_revenue = statistics.mean(revenue_history)
    stdev_revenue = statistics.stdev(revenue_history)
    
    # Flag if this week is more than 1.5 std devs away from mean
    if abs(metrics['revenue'] - mean_revenue) > 1.5 * stdev_revenue:
        direction = "above" if metrics['revenue'] > mean_revenue else "below"
        anomalies.append({
            "metric": "Revenue",
            "value": metrics['revenue'],
            "baseline": mean_revenue,
            "direction": direction,
            "severity": "high"
        })
    
    # Flag if AOV dropped significantly
    aov_history = [m['avg_order_value'] for m in historical_data[-12:]]
    aov_mean = statistics.mean(aov_history)
    if metrics['avg_order_value'] < aov_mean * 0.9:  # 10% drop
        anomalies.append({
            "metric": "Average Order Value",
            "value": metrics['avg_order_value'],
            "baseline": aov_mean,
            "direction": "below",
            "severity": "medium"
        })
    
    return anomalies

anomalies = detect_anomalies(metrics, historical_weeks)
print(anomalies)

Now pass anomalies to your narrative prompt explicitly:

python
def generate_report_with_anomalies(metrics, anomalies, narrative):
    """Enhance narrative by flagging anomalies."""
    if not anomalies:
        return narrative
    
    anomaly_text = "\n".join([
        f"- {a['metric']}: {a['direction']} normal (current: {a['value']:.2f}, typical: {a['baseline']:.2f})"
        for a in anomalies
    ])
    
    client = anthropic.Anthropic()
    prompt = f"""{narrative}

Also flag these anomalies for the reader:
{anomaly_text}

Add 1-2 sentences explaining the most critical anomaly and what it might indicate."""
    
    message = client.messages.create(
        model="claude-3-5-sonnet-20241022",
        max_tokens=400,
        messages=[
            {"role": "user", "content": prompt}
        ]
    )
    
    return message.content[0].text

Anomalies draw attention where it matters. A revenue spike gets buried; a revenue spike with a flagged explanation gets acted on.

Common mistakes

  • Asking the model to compute aggregations. Models hallucinate math. Always compute in application code and pass clean numbers.
  • Passing raw data to the model. A dataset with thousands of rows creates context bloat, increases token costs, and reduces accuracy. Pre-aggregate everything.
  • Using generic prompts for every report type. Different stakeholders need different focus. Product teams care about feature adoption; finance teams care about margins. Tailor the prompt to the audience.
  • Treating AI output as final without validation. Generate the report, but validate structure and sanity-check narratives. A model might produce grammatically perfect nonsense.
  • Forgetting to version metrics. If your calculation logic changes, reports change retroactively. Store the calculation version or timestamp with each report so you can audit differences.

Key takeaways

  • Compute all metrics in application code using deterministic databases and cache layers, never in the model.
  • Keep the model's scope narrow: narrative generation and tone adaptation, not calculation or logic.
  • Define report structure as a template or schema before populating content, ensuring consistency and validation.
  • Detect anomalies and changes at the metric layer, then pass flagged items to the model for narrative emphasis.
  • Validate model output before delivery; treat it as content that's well-written but not guaranteed correct.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles