
工具链搭建 | 适合:每周手动整理报表的数据分析师、接私单的自由职业者
痛点:一份报告从2小时到5分钟
很多数据分析师接私单时,最耗时的环节不是分析,而是把分析结果排版成漂亮的PDF交付给客户。每次手动做报表,成本高、易出错、客户满意度还低。
用 DuckDB + Python + ReportLab,可以把这个流程从2小时压缩到5分钟,并且支持批量生成不同客户的报告,边际成本趋近于零。
第一步:用 DuckDB 完成核心分析
import duckdb
import pandas as pd
con = duckdb.connect()
# read_csv_auto 自动推断列类型,无需提前定义 schema
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))
DuckDB 相比传统方式的优势:
| 方式 | 读取CSV | 类型推断 | 聚合性能 |
|---|---|---|---|
| Pandas + 手写 | 需指定列名 | 手动 astype | 需循环 |
| SQLite | 需先导入 | 需建表定义 | 一般 |
| DuckDB | read_csv_auto 一行搞定 | 全自动 | 列式存储,毫秒级 |
read_csv_auto 是 DuckDB 的杀手级功能——直接读 CSV 文件,自动推断分隔符、列类型、日期格式,省掉所有预处理步骤。对于多文件场景,还支持 glob 通配符批量合并:read_csv_auto('sales_data/*.csv')。
第二步:生成报告图表
import matplotlib.pyplot as plt
import matplotlib
matplotlib.use('Agg') # 无头模式,适合服务器部署
fig, axes = plt.subplots(2, 2, figsize=(14, 10))
fig.suptitle('月度销售数据分析报告', fontsize=16, fontweight='bold')
# 1. 各品类月度收入趋势
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('品类收入趋势')
axes[0, 0].tick_params(axis='x', rotation=45)
# 2. 本月品类收入占比
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} 品类收入占比')
# 3. 客单价分布
axes[1, 0].hist(result['avg_order_value'], bins=20, color='steelblue', alpha=0.7)
axes[1, 0].set_title('平均客单价分布')
axes[1, 0].set_xlabel('金额 (元)')
# 4. 客户数增长
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('各品类月度客户数')
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('图表已生成')
第三步:打包成专业 PDF 报告
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
# 注册中文字体
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 = []
# 标题
story.append(Paragraph('月度销售数据分析报告', title_style))
story.append(Paragraph(f'生成日期:{pd.Timestamp.now().strftime("%Y年%m月%d日")}', body_style))
story.append(Spacer(1, 0.5*cm))
# 执行摘要
total_revenue = result['total_revenue'].sum()
total_orders = result['order_count'].sum()
avg_value = result['avg_order_value'].mean()
story.append(Paragraph('一、执行摘要', heading_style))
summary_text = f"""本次分析基于 {len(result)} 条记录,涵盖 {result['product_category'].nunique()} 个产品品类。
关键指标:总营收 {total_revenue:,.0f} 元,订单数 {total_orders:,} 笔,平均客单价 {avg_value:.2f} 元。"""
story.append(Paragraph(summary_text, body_style))
story.append(Spacer(1, 0.5*cm))
# 图表
story.append(Paragraph('二、数据可视化分析', heading_style))
story.append(Image('report_charts.png', width=450, height=320))
story.append(Spacer(1, 0.5*cm))
# Top 10 数据表
story.append(Paragraph('三、品类明细 Top 10', heading_style))
top10 = result.nlargest(10, 'total_revenue')
table_data = [['月份', '品类', '订单数', '总收入', '平均客单价']]
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报告已生成:sales_report.pdf')
进阶:打造可复用的配置驱动 Pipeline
上面的代码可以封装成配置驱动的 Pipeline,只需修改 YAML 配置文件即可切换不同客户:
import yaml
import json
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)
# DuckDB 执行分析
con = duckdb.connect()
sql = config['sql_template'].format(
date_range=config.get('date_range', 'last_30_days')
)
result = con.sql(sql).df()
# 渲染 PDF
output_name = f"reports/{config['name']}_{datetime.now().strftime('%Y%m%d')}.pdf"
render_pdf(result, config, output_path=output_name)
print(f'✅ {config["name"]} 报告已生成')
# clients/ecommerce_a.yaml:
# name: 电商客户A
# sql_template: "SELECT * FROM read_csv_auto('orders_a.csv') WHERE ..."
# date_range: last_30_days
# 改配置不改代码,一天批量生成10+份报告
变现路径:一份报告能赚多少钱
| 模式 | 单次收费 | 月均可复制 | 年化收入参考 |
|---|---|---|---|
| 单次交付 | 500-2,000元 | 20-40份 | 12-96万 |
| 订阅制 | 年费 5,000-20,000元/客户 | 5-10客户 | 5-20万/年 |
| SaaS平台 | 自助报告,按次收费 | 无限放大 | 边际成本趋近0 |
核心逻辑:DuckDB 负责数据处理(快、准、省资源),Python + ReportLab 负责渲染输出,YAML 配置实现多客户复用。三者结合后,单次报告的边际成本几乎为零。
与传统工具的对比
| 工具 | 读取CSV | SQL聚合 | PDF生成 | 中文字体 | 配置驱动 |
|---|---|---|---|---|---|
| Excel + VBA | 手动 | 透视表 | 打印导出 | 复杂 | 难 |
| Pandas + matplotlib | 需列定义 | 需写代码 | 需额外库 | 需处理 | 中等 |
| Power BI | 图形界面 | DAX | 导出PDF | 内置 | 模板驱动 |
| DuckDB + Python | 自动推断 | SQL一行 | ReportLab | TTFont注册 | YAML配置 |
结尾:下一步行动建议
- 立即上手:准备一份 CSV 数据,复制上面的三段代码,跑通全流程
- 迭代优化:根据你的报告模板调整 ReportLab 样式,加入公司 Logo 和页眉页脚
- 规模化:用 YAML 配置驱动多客户,一次开发,批量输出
- 变现:先从一个真实客户开始,用自动化报告替代手工报表,验证付费意愿
📖 详细图文教程见 duckdblab.org