Featured image of post DuckDB Automated PDF Report Generation: From CSV to Sellable Data Reports

DuckDB Automated PDF Report Generation: From CSV to Sellable Data Reports

Build a one-click professional PDF report pipeline using DuckDB + Python + ReportLab. Covers CSV auto-detection, SQL aggregation, matplotlib charts, Chinese font PDF layout, and monetization strategies.

DuckDB Automated Report Pipeline

Toolchain | For: Data analysts tired of manual monthly reporting


The Pain: From 2 Hours to 5 Minutes Per Report

Many data analysts doing freelance work spend the most time not on analysis, but on formatting results into polished PDFs for clients. Manual reports are costly, error-prone, and leave clients unsatisfied.

With DuckDB + Python + ReportLab, this entire workflow compresses from 2 hours down to 5 minutes, supporting batch generation across multiple clients with near-zero marginal cost.


Step 1: Core Analysis with DuckDB

import duckdb
import pandas as pd

con = duckdb.connect()

# read_csv_auto infers column types automatically—no schema needed
analysis_sql = """
SELECT
    DATE_TRUNC('month', order_date) AS month,
    product_category,
    COUNT(*) AS order_count,
    SUM(amount) AS total_revenue,
    AVG(amount) AS avg_order_value,
    COUNT(DISTINCT customer_id) AS unique_customers
FROM read_csv_auto('orders.csv')
GROUP BY month, product_category
ORDER BY month DESC, total_revenue DESC
"""

result = con.sql(analysis_sql).df()
print(result.head(10))

Why DuckDB beats traditional approaches:

ApproachCSV ReadingType InferenceAggregation Speed
Pandas + manualSpecify columnsManual astypeLoop-based
SQLiteImport firstTable schema requiredAverage
DuckDBread_csv_auto one-linerFully automaticColumnar, millisecond

read_csv_auto is DuckDB’s killer feature—read any CSV directly, auto-detect delimiters, column types, and date formats. For multi-file scenarios, glob patterns merge everything: read_csv_auto('sales_data/*.csv').


Step 2: Generate Chart Visualizations

import matplotlib.pyplot as plt
import matplotlib
matplotlib.use('Agg')  # headless mode for server deployment

fig, axes = plt.subplots(2, 2, figsize=(14, 10))
fig.suptitle('Monthly Sales Analysis Report', fontsize=16, fontweight='bold')

# 1. Revenue trend by category
pivot_data = result.pivot(index='month', columns='product_category',
                           values='total_revenue')
pivot_data.plot(kind='line', ax=axes[0, 0], marker='o')
axes[0, 0].set_title('Revenue Trend by Category')
axes[0, 0].tick_params(axis='x', rotation=45)

# 2. Pie chart for latest month
latest_month = result['month'].max()
monthly_data = result[result['month'] == latest_month]
monthly_data.plot.pie(y='total_revenue', ax=axes[0, 1],
                       labels=monthly_data['product_category'],
                       autopct='%1.1f%%')
axes[0, 1].set_title(f'{latest_month} Revenue Share')

# 3. Order value distribution
axes[1, 0].hist(result['avg_order_value'], bins=20,
                color='steelblue', alpha=0.7)
axes[1, 0].set_title('Average Order Value Distribution')
axes[1, 0].set_xlabel('Amount (CNY)')

# 4. Customer count by category
pivot_customers = result.pivot(index='month',
                                columns='product_category',
                                values='unique_customers')
pivot_customers.plot.bar(stacked=True, ax=axes[1, 1])
axes[1, 1].set_title('Monthly Customers by Category')
axes[1, 1].tick_params(axis='x', rotation=45)

plt.tight_layout()
plt.savefig('report_charts.png', dpi=150, bbox_inches='tight')
plt.close()
print('Charts generated')

Step 3: Assemble Professional PDF Report

from reportlab.lib import colors
from reportlab.lib.pagesizes import A4
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, \
    Image, Table, TableStyle
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.units import cm
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont

# Register Chinese font
try:
    pdfmetrics.registerFont(TTFont('Chinese',
        '/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc'))
    font_name = 'Chinese'
except:
    font_name = 'Helvetica'

doc = SimpleDocTemplate('sales_report.pdf', pagesize=A4,
                        rightMargin=2*cm, leftMargin=2*cm,
                        topMargin=2*cm, bottomMargin=2*cm)

styles = getSampleStyleSheet()
title_style = ParagraphStyle('Title', parent=styles['Title'],
                              fontName=font_name, fontSize=18,
                              spaceAfter=20)
heading_style = ParagraphStyle('Heading', parent=styles['Heading1'],
                                fontName=font_name, fontSize=14,
                                spaceAfter=10)
body_style = ParagraphStyle('Body', parent=styles['Normal'],
                             fontName=font_name, fontSize=10,
                             leading=16)

story = []

# Title
story.append(Paragraph('Monthly Sales Analysis Report', title_style))
story.append(Paragraph(
    f'Generated: {pd.Timestamp.now().strftime("%Y-%m-%d")}', body_style))
story.append(Spacer(1, 0.5*cm))

# Executive summary
total_revenue = result['total_revenue'].sum()
total_orders = result['order_count'].sum()
avg_value = result['avg_order_value'].mean()
story.append(Paragraph('I. Executive Summary', heading_style))
summary = (f'Analysis covers {len(result)} records across '
           f'{result["product_category"].nunique()} categories. '
           f'Total revenue: {total_revenue:,.0f} CNY, '
           f'{total_orders:,} orders, average order value: '
           f'{avg_value:.2f} CNY.')
story.append(Paragraph(summary, body_style))
story.append(Spacer(1, 0.5*cm))

# Charts
story.append(Paragraph('II. Data Visualization', heading_style))
story.append(Image('report_charts.png', width=450, height=320))
story.append(Spacer(1, 0.5*cm))

# Top 10 table
story.append(Paragraph('III. Top 10 Categories', heading_style))
top10 = result.nlargest(10, 'total_revenue')
table_data = [['Month', 'Category', 'Orders', 'Revenue', 'Avg Value']]
for _, row in top10.iterrows():
    table_data.append([str(row['month'])[:10], row['product_category'],
                       str(int(row['order_count'])),
                       f"{row['total_revenue']:,.0f}",
                       f"{row['avg_order_value']:.2f}"])

table = Table(table_data, colWidths=[2.5*cm, 3*cm, 2*cm, 3*cm, 3*cm])
table.setStyle(TableStyle([
    ('BACKGROUND', (0, 0), (-1, 0), colors.HexColor('#2C3E50')),
    ('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke),
    ('ALIGN', (0, 0), (-1, -1), 'CENTER'),
    ('FONTNAME', (0, 0), (-1, 0), font_name),
    ('FONTSIZE', (0, 0), (-1, 0), 10),
    ('BOTTOMPADDING', (0, 0), (-1, 0), 12),
    ('GRID', (0, 0), (-1, -1), 1, colors.grey),
    ('BACKGROUND', (0, 1), (-1, -1), colors.HexColor('#ECF0F1')),
]))
story.append(table)

doc.build(story)
print('PDF report generated: sales_report.pdf')

Advanced: Configuration-Driven Pipeline

Wrap the above into a config-driven pipeline. Switch clients by changing a YAML file—no code modification needed:

import yaml
from datetime import datetime

def run_report(client_config_path='clients/default.yaml'):
    with open(client_config_path) as f:
        config = yaml.safe_load(f)

    con = duckdb.connect()
    sql = config['sql_template'].format(
        date_range=config.get('date_range', 'last_30_days')
    )
    result = con.sql(sql).df()

    output_name = (f"reports/{config['name']}_"
                   f"{datetime.now().strftime('%Y%m%d')}.pdf")
    render_pdf(result, config, output_path=output_name)
    print(f"✅ {config['name']} report generated")

# clients/ecommerce_a.yaml:
# name: Ecommerce Client A
# sql_template: "SELECT * FROM read_csv_auto('orders_a.csv') WHERE ..."
# date_range: last_30_days

# One configuration change, batch-generate 10+ reports per day

Monetization: How Much Can a Report Be Worth?

ModelPer-Report PriceMonthly ReplicabilityAnnual Revenue
One-off delivery500-2,000 CNY20-40 reports120K-960K CNY
Subscription5,000-20,000 CNY/year5-10 clients50K-200K CNY
SaaS platformPay-per-reportInfinite scaleNear-zero marginal cost

Core insight: DuckDB handles data processing (fast, accurate, lightweight), Python + ReportLab handles rendering, YAML configuration enables multi-client reuse. Together, the marginal cost per report approaches zero.


Comparison with Traditional Tools

ToolCSV ReadingSQL AggregationPDF GenerationChinese FontsConfig-Driven
Excel + VBAManualPivot tablesPrint/exportComplexHard
Pandas + matplotlibColumn definition neededCode-heavyExtra libraryRequires handlingMedium
Power BIGUI importDAXExport PDFBuilt-inTemplate-driven
DuckDB + PythonAuto-detectOne SQL lineReportLabTTFont registerYAML config

Action Plan

  1. Get started now: Prepare a CSV file, copy the three code blocks above, run the full pipeline
  2. Iterate: Customize ReportLab styles with your company logo and headers
  3. Scale up: Use YAML config for multiple clients—one development, batch output
  4. Monetize: Start with one real client, replace manual reporting, validate willingness to pay

💡 More DuckDB automation tutorials → duckdblab.org

📺 Watch video tutorials → Olap Studio YouTube

Subscribe for more DuckDB & AI automation tutorials

Built with Hugo
Theme Stack designed by Jimmy

⚠️ This site is an independent community project, not affiliated with, endorsed by, or sponsored by the DuckDB Foundation or official DuckDB project.

"DuckDB" is a registered trademark of the DuckDB Foundation. This site uses the name solely for factual description purposes.

All content is for educational and community promotion purposes only and does not constitute any commercial service.