DuckDB End-to-End Automated Report Pipeline: From Data Source to Email Delivery
Toolchain | For: Data analysts who manually compile reports every week
The Pain Point
You spend 3 hours every Monday morning manually compiling Excel reports — repetitive, boring, and error-prone. With a DuckDB-based automated reporting system, this workflow compresses to 5 minutes, or even runs fully autonomously.
This article walks you through building a complete DuckDB + Python automated report pipeline, covering data ingestion, analytical queries, formatted output, scheduled execution, and email delivery.
System Architecture Overview
Data Sources (Excel/CSV/SQL)
↓
DuckDB (Unified Query Layer)
↓
Python Processing (Cleaning/Aggregation)
↓
Output (Excel/PDF/Email Delivery)
Key advantages: DuckDB reads Excel and CSV files natively without intermediate conversion; SQL表达能力 strong for complex aggregations in a single query; combined with Python’s ecosystem, it can string together an entire workflow.
Step 1: Environment Setup
pip install duckdb openpyxl pandas
Verify installation:
import duckdb
print(duckdb.__version__) # Should print the version number
Step 2: Connect to Data Sources
Assume you have an e-commerce sales data folder sales_data/ containing multiple CSV files, each representing one month of sales records.
import duckdb
import pandas as pd
from pathlib import Path
# Connect to a persistent DuckDB database (data saved to disk)
con = duckdb.connect('sales_automation.duckdb')
# Batch-read all CSV files (auto-merge, no need to read individually)
con.execute("""
CREATE TABLE all_sales AS
SELECT * FROM read_csv_auto('sales_data/*.csv', hive_partitioning=true)
""")
# Preview the data structure
result = con.execute("DESCRIBE all_sales").fetchdf()
print(result)
Key techniques:
read_csv_autoautomatically infers column types — no need to manually specify schemashive_partitioning=truesupports automatic partitioning by directory structure; dates in filenames are extracted as partition columns- DuckDB’s columnar storage makes aggregation queries 10x faster than Pandas
Step 3: Core Analytical Queries
3.1 Monthly Sales Summary
monthly_summary = con.execute("""
SELECT
DATE_TRUNC('month', sale_date) AS month,
SUM(revenue) AS total_revenue,
COUNT(DISTINCT customer_id) AS active_customers,
AVG(order_value) AS avg_order_value,
SUM(CASE WHEN region = 'East' THEN revenue ELSE 0 END) AS east_region_revenue
FROM all_sales
GROUP BY DATE_TRUNC('month', sale_date)
ORDER BY month DESC
""").fetchdf()
print(monthly_summary)
3.2 Top Products Ranking (with window functions)
top_products = con.execute("""
SELECT
product_name,
category,
SUM(quantity) AS total_qty,
SUM(revenue) AS total_revenue,
RANK() OVER (ORDER BY SUM(revenue) DESC) AS revenue_rank
FROM all_sales
GROUP BY product_name, category
HAVING SUM(revenue) > 1000
ORDER BY total_revenue DESC
LIMIT 20
""").fetchdf()
3.3 Customer Segmentation
customer_segment = con.execute("""
SELECT
customer_id,
COUNT(*) AS order_count,
SUM(revenue) AS total_spend,
MAX(sale_date) AS last_purchase,
CASE
WHEN SUM(revenue) > 10000 THEN 'VIP'
WHEN SUM(revenue) > 5000 THEN 'High-Value'
WHEN SUM(revenue) > 1000 THEN 'Regular'
ELSE 'Potential'
END AS segment
FROM all_sales
GROUP BY customer_id
ORDER BY total_spend DESC
""").fetchdf()
Step 4: Generate Formatted Reports
4.1 Excel Format (Styled)
import openpyxl
from openpyxl.styles import Font, PatternFill, Alignment
def generate_excel_report(df_dict, output_path='report.xlsx'):
with pd.ExcelWriter(output_path, engine='openpyxl') as writer:
for sheet_name, df in df_dict.items():
df.to_excel(writer, sheet_name=sheet_name, index=False)
# Style the header row
ws = writer.sheets[sheet_name]
header_fill = PatternFill(start_color='4472C4', end_color='4472C4', fill_type='solid')
header_font = Font(bold=True, color='FFFFFF')
for cell in ws[1]:
cell.fill = header_fill
cell.font = header_font
cell.alignment = Alignment(horizontal='center')
# Auto-fit column widths
for column in ws.columns:
max_length = 0
column_letter = column[0].column_letter
for cell in column:
try:
if len(str(cell.value)) > max_length:
max_length = len(str(cell.value))
except:
pass
adjusted_width = min(max_length + 2, 50)
ws.column_dimensions[column_letter].width = adjusted_width
print(f'Report generated: {output_path}')
# Generate multi-sheet report
report_data = {
'Monthly Summary': monthly_summary,
'Top Products': top_products,
'Customer Segments': customer_segment
}
generate_excel_report(report_data, 'weekly_report.xlsx')
4.2 Plain Text Format (Minimal)
def generate_text_report(df_dict, output_path='report.txt'):
with open(output_path, 'w', encoding='utf-8') as f:
f.write('=' * 60 + '\n')
f.write('Weekly Data Report\n')
f.write('=' * 60 + '\n\n')
for sheet_name, df in df_dict.items():
f.write(f'\n[{sheet_name}]\n')
f.write(df.to_string(index=False))
f.write('\n' + '-' * 60 + '\n')
print(f'Text report generated: {output_path}')
Step 5: Scheduled Execution (Three Options)
Option A: Python schedule library (local execution)
import schedule
import time
from datetime import datetime
def run_weekly_report():
print(f'{datetime.now()} - Starting weekly report...')
# Reload latest data
con.execute("DROP TABLE IF EXISTS all_sales")
con.execute("""
CREATE TABLE all_sales AS
SELECT * FROM read_csv_auto('sales_data/*.csv', hive_partitioning=true)
""")
# Execute analysis queries
monthly_summary = con.execute("""
SELECT DATE_TRUNC('month', sale_date) AS month,
SUM(revenue) AS total_revenue, COUNT(DISTINCT customer_id) AS active_customers
FROM all_sales GROUP BY DATE_TRUNC('month', sale_date)
ORDER BY month DESC
""").fetchdf()
# Output report
report_data = {'Monthly Summary': monthly_summary}
generate_excel_report(report_data, f'report_{datetime.now().strftime("%Y%m%d")}.xlsx')
print('Weekly report generated!')
# Run every Monday at 9:00 AM
schedule.every().monday.at("09:00").do(run_weekly_report)
while True:
schedule.run_pending()
time.sleep(60)
Option B: System Cron (Linux/Mac)
crontab -e
Add this line (runs every Monday at 9:00 AM):
0 9 * * 1 cd /path/to/project && python3 weekly_report.py
Option C: GitHub Actions (cloud-based, free)
Create .github/workflows/report.yml:
name: Weekly Report
on:
schedule:
- cron: '0 9 * * 1' # Monday 9:00 UTC
workflow_dispatch:
jobs:
build-report:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.11'
- name: Install dependencies
run: pip install duckdb openpyxl pandas
- name: Generate report
run: python3 weekly_report.py
- name: Upload artifact
uses: actions/upload-artifact@v3
with:
name: weekly-report
path: report_*.xlsx
Step 6: Email Delivery (Optional)
import smtplib
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.mime.base import MIMEBase
from email import encoders
def send_email_report(to_email, report_path, subject='Weekly Report'):
msg = MIMEMultipart()
msg['From'] = '[email protected]'
msg['To'] = to_email
msg['Subject'] = subject
body = 'Please find this week\'s data report attached.'
msg.attach(MIMEText(body, 'plain', 'utf-8'))
with open(report_path, 'rb') as f:
attachment = MIMEBase('application', 'octet-stream')
attachment.set_payload(f.read())
encoders.encode_base64(attachment)
attachment.add_header('Content-Disposition', f'attachment; filename={report_path}')
msg.attach(attachment)
with smtplib.SMTP('smtp.example.com', 587) as server:
server.starttls()
server.login('[email protected]', 'your_password')
server.send_message(msg)
print(f'Email sent to {to_email}')
Comparison with Traditional Approaches
| Dimension | Traditional Pandas Approach | DuckDB + Python Approach |
|---|---|---|
| Multi-file reading | Must read_csv + concat one by one | read_csv_auto('*.csv') in one line |
| Memory usage | All data loaded into memory | Columnar storage, compute-on-demand |
| Query performance | Noticeably slow above 1M rows | Sub-second response for millions of rows |
| File support | Requires additional libraries | Native support for CSV/Parquet/JSON/Excel/SQLite/PostgreSQL |
| Deployment complexity | Dependencies on Pandas + multiple libs | Single DuckDB dependency |
Monetization Suggestions
The value of this automated reporting pipeline goes far beyond saving time:
- SaaS Product: Package the reporting capability as an API service, charge per call — monthly income from thousands to tens of thousands of dollars
- Data Product: Provide industry data report subscription services for SMEs with annual billing for stable income
- Internal Tooling: Promote this system across your team, become the data infrastructure lead, and accelerate your career
- Consulting & Training: Convert your实战 experience into courses or consulting services, charging 500-5000 RMB per client
The core idea: transform one-off reporting work into reusable data products.
Learn more DuckDB实战 tips → duckdblab.org
