Build A Personal AI Agent – Episode 10
PDF Export with Automated Formatting for Reports
A Practical Guide for Evaluators and M&E Professionals
Why PDF Export for Automated Reports
PDF is the standard format for professional report distribution. This tutorial shows you how to generate polished PDF reports with automated formatting, embedded charts, tables, and professional styling—ready for sharing with donors, stakeholders, and partners.
Part 1: Setting Up PDF Generation
Step 1: Install Required Libraries
pip install reportlab PyPDF2 Pillow matplotlib seaborn pandas openpyxlLibrary Guide:
reportlab: Core PDF generation library with extensive formatting options
Pillow: Image processing for charts and logos
matplotlib/seaborn: Chart generation for PDF embedding
Step 2: PDF Configuration
# pdf_config.py
from reportlab.lib.pagesizes import letter, A4
from reportlab.lib.units import inch, cm
from reportlab.lib import colors
from reportlab.lib.enums import TA_CENTER, TA_LEFT, TA_RIGHT
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.fonts import addMapping
class PDFConfig:
"""Configuration for PDF report generation."""
# Page settings
PAGE_SIZE = A4
MARGIN_TOP = 1.5 * cm
MARGIN_BOTTOM = 1.5 * cm
MARGIN_LEFT = 1.8 * cm
MARGIN_RIGHT = 1.8 * cm
# Colors (M&E theme)
COLORS = {
'primary': colors.HexColor('#1a1a2e'),
'secondary': colors.HexColor('#4a9eff'),
'accent': colors.HexColor('#6c5ce7'),
'success': colors.HexColor('#28a745'),
'warning': colors.HexColor('#ffc107'),
'danger': colors.HexColor('#dc3545'),
'light': colors.HexColor('#f8f9fa'),
'dark': colors.HexColor('#343a40'),
'text': colors.HexColor('#333333'),
'border': colors.HexColor('#e9ecef')
}
# Font settings
FONT_FAMILY = 'Helvetica'
FONT_SIZES = {
'title': 24,
'heading1': 18,
'heading2': 16,
'heading3': 14,
'body': 11,
'small': 9,
'caption': 8
}
@classmethod
def get_styles(cls):
"""Get custom paragraph styles."""
styles = getSampleStyleSheet()
# Title style
styles.add(ParagraphStyle(
name='ReportTitle',
parent=styles['Title'],
fontName='Helvetica-Bold',
fontSize=cls.FONT_SIZES['title'],
textColor=cls.COLORS['primary'],
alignment=TA_CENTER,
spaceAfter=20,
spaceBefore=10
))
# Heading 1
styles.add(ParagraphStyle(
name='Heading1',
parent=styles['Heading1'],
fontName='Helvetica-Bold',
fontSize=cls.FONT_SIZES['heading1'],
textColor=cls.COLORS['secondary'],
spaceAfter=12,
spaceBefore=16,
borderPadding=5,
borderWidth=0,
leftIndent=0
))
# Heading 2
styles.add(ParagraphStyle(
name='Heading2',
parent=styles['Heading2'],
fontName='Helvetica-Bold',
fontSize=cls.FONT_SIZES['heading2'],
textColor=cls.COLORS['primary'],
spaceAfter=8,
spaceBefore=12
))
# Body text
styles.add(ParagraphStyle(
name='BodyText',
parent=styles['Normal'],
fontName='Helvetica',
fontSize=cls.FONT_SIZES['body'],
textColor=cls.COLORS['text'],
leading=16,
alignment=TA_LEFT,
spaceAfter=6
))
return stylesPart 2: Building the PDF Generator
PDF Report Generator
import os
from datetime import datetime
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle, Image, PageBreak
from reportlab.lib.pagesizes import A4
from reportlab.lib.units import cm
from reportlab.lib import colors
import matplotlib.pyplot as plt
import seaborn as sns
import pandas as pd
import io
class PDFReportGenerator:
"""Generate professional PDF reports with charts and tables."""
def __init__(self, config=None, output_dir='reports'):
self.config = config or PDFConfig()
self.styles = self.config.get_styles()
self.output_dir = output_dir
os.makedirs(output_dir, exist_ok=True)
self.elements = []
self.chart_paths = []
def generate_report(self, df, metrics, title, subtitle=None, include_charts=True):
"""Generate complete PDF report."""
# Clear previous elements
self.elements = []
self.chart_paths = []
# Set up document
filename = f"{self.output_dir}/report_{datetime.now().strftime('%Y%m%d_%H%M')}.pdf"
doc = SimpleDocTemplate(
filename,
pagesize=self.config.PAGE_SIZE,
topMargin=self.config.MARGIN_TOP,
bottomMargin=self.config.MARGIN_BOTTOM,
leftMargin=self.config.MARGIN_LEFT,
rightMargin=self.config.MARGIN_RIGHT
)
# Build content
self._add_title(title, subtitle)
self._add_summary(metrics)
if include_charts:
self._add_charts(df)
self._add_data_table(df)
self._add_recommendations(df)
self._add_footer()
# Build PDF
doc.build(self.elements)
print(f"PDF report generated: {filename}")
return filename
def _add_title(self, title, subtitle=None):
"""Add title section to PDF."""
self.elements.append(Paragraph(title, self.styles['ReportTitle']))
if subtitle:
self.elements.append(Paragraph(
subtitle,
ParagraphStyle(
name='Subtitle',
parent=self.styles['Normal'],
fontName='Helvetica',
fontSize=11,
textColor=self.config.COLORS['dark'],
alignment=TA_CENTER,
spaceAfter=15
)
))
# Date
date_str = f"Generated: {datetime.now().strftime('%B %d, %Y at %H:%M')}"
self.elements.append(Paragraph(date_str, self.styles['Normal']))
self.elements.append(Spacer(1, 20))
# Divider line
self._add_divider()
def _add_divider(self):
"""Add a horizontal divider line."""
from reportlab.platypus import HRFlowable
self.elements.append(HRFlowable(
width="100%",
thickness=2,
color=self.config.COLORS['secondary'],
spaceBefore=10,
spaceAfter=10
))
def _add_summary(self, metrics):
"""Add key metrics summary section."""
self.elements.append(Paragraph("Executive Summary", self.styles['Heading1']))
# Summary text
total = metrics.get('total_indicators', 0)
on_track = metrics.get('on_track', 0)
at_risk = metrics.get('at_risk', 0)
off_track = metrics.get('off_track', 0)
avg_achievement = metrics.get('avg_achievement', 0)
summary_text = f"""
This report covers {total} indicators with an overall achievement of {avg_achievement:.1f}%.
{on_track} indicators are on track, {at_risk} require attention,
and {off_track} are off track and need immediate intervention.
"""
self.elements.append(Paragraph(summary_text.strip(), self.styles['BodyText']))
self.elements.append(Spacer(1, 10))
# Key metrics table
data = [
['Metric', 'Value'],
['Total Indicators', str(total)],
['On Track', str(on_track)],
['At Risk', str(at_risk)],
['Off Track', str(off_track)],
['Average Achievement', f"{avg_achievement:.1f}%"]
]
table = Table(data, colWidths=[4*cm, 3*cm])
table.setStyle(TableStyle([
('BACKGROUND', (0, 0), (1, 0), self.config.COLORS['secondary']),
('TEXTCOLOR', (0, 0), (1, 0), colors.whitesmoke),
('ALIGN', (0, 0), (1, 0), 'CENTER'),
('FONTNAME', (0, 0), (1, 0), 'Helvetica-Bold'),
('FONTSIZE', (0, 0), (1, 0), 12),
('BOTTOMPADDING', (0, 0), (1, 0), 8),
('BACKGROUND', (0, 1), (1, 5), self.config.COLORS['light']),
('GRID', (0, 0), (1, 5), 0.5, self.config.COLORS['border']),
('FONTNAME', (0, 1), (1, 5), 'Helvetica'),
('FONTSIZE', (0, 1), (1, 5), 10),
('PADDING', (0, 0), (1, 5), 6),
]))
self.elements.append(table)
self.elements.append(Spacer(1, 20))
def _add_charts(self, df):
"""Generate and embed charts in PDF."""
self.elements.append(Paragraph("Performance Visualizations", self.styles['Heading1']))
# Generate charts using matplotlib
charts = self._create_charts(df)
# Add charts to PDF
for chart_info in charts:
img = Image(chart_info['path'], width=15*cm, height=10*cm)
self.elements.append(Paragraph(chart_info['title'], self.styles['Heading2']))
self.elements.append(img)
self.elements.append(Spacer(1, 10))
self.elements.append(PageBreak())
def _create_charts(self, df):
"""Create charts for PDF embedding."""
charts = []
# 1. Target vs Actual Chart
if 'target' in df.columns and 'actual' in df.columns:
fig, ax = plt.subplots(figsize=(10, 6))
x = range(len(df))
width = 0.35
ax.bar(x, df['target'], width, label='Target', color='#4a9eff')
ax.bar([i + width for i in x], df['actual'], width, label='Actual', color='#6c5ce7')
ax.set_xlabel('Indicators')
ax.set_ylabel('Values')
ax.set_title('Target vs Actual Performance')
ax.set_xticks([i + width/2 for i in x])
ax.set_xticklabels(df['indicator_name'], rotation=45, ha='right')
ax.legend()
ax.grid(axis='y', alpha=0.3)
plt.tight_layout()
path = self._save_chart(fig, 'target_actual')
charts.append({'path': path, 'title': 'Target vs Actual Performance'})
plt.close(fig)
# 2. Achievement Gauge (if achievement column exists)
if 'achievement' in df.columns:
avg_achievement = df['achievement'].mean()
fig, ax = plt.subplots(figsize=(8, 6))
# Create gauge using pie chart
values = [avg_achievement, 100 - avg_achievement]
colors_gauge = ['#28a745' if avg_achievement >= 80 else '#ffc107' if avg_achievement >= 60 else '#dc3545', '#e9ecef']
ax.pie(values, colors=colors_gauge, startangle=90, wedgeprops={'width': 0.3})
ax.text(0, 0, f'{avg_achievement:.0f}%', ha='center', va='center', fontsize=24, fontweight='bold')
ax.text(0, -0.15, 'Overall Achievement', ha='center', va='center', fontsize=14, color='#666')
ax.set_title('Program Performance', fontsize=16, fontweight='bold')
plt.tight_layout()
path = self._save_chart(fig, 'gauge')
charts.append({'path': path, 'title': 'Overall Achievement Gauge'})
plt.close(fig)
# 3. Status Distribution (if status column exists)
if 'status' in df.columns:
status_counts = df['status'].value_counts()
fig, ax = plt.subplots(figsize=(8, 6))
colors_status = {
'on_track': '#28a745',
'at_risk': '#ffc107',
'off_track': '#dc3545',
'completed': '#4a9eff'
}
pie_colors = [colors_status.get(s, '#6c757d') for s in status_counts.index]
ax.pie(status_counts.values, labels=status_counts.index,
autopct='%1.1f%%', colors=pie_colors, startangle=90)
ax.set_title('Status Distribution')
plt.tight_layout()
path = self._save_chart(fig, 'status_pie')
charts.append({'path': path, 'title': 'Indicator Status Distribution'})
plt.close(fig)
# 4. Trend Chart (if date column exists)
if 'date' in df.columns and 'achievement' in df.columns:
df_trend = df.groupby('date')['achievement'].mean().reset_index()
fig, ax = plt.subplots(figsize=(10, 6))
ax.plot(df_trend['date'], df_trend['achievement'], marker='o', color='#4a9eff', linewidth=2)
ax.axhline(y=80, color='#ffc107', linestyle='--', label='Target (80%)')
ax.set_xlabel('Date')
ax.set_ylabel('Achievement (%)')
ax.set_title('Progress Trend Over Time')
ax.legend()
ax.grid(True, alpha=0.3)
ax.tick_params(axis='x', rotation=45)
plt.tight_layout()
path = self._save_chart(fig, 'trend')
charts.append({'path': path, 'title': 'Progress Trend'})
plt.close(fig)
return charts
def _save_chart(self, fig, name):
"""Save chart as image for PDF embedding."""
path = f"{self.output_dir}/chart_{name}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.png"
fig.savefig(path, dpi=150, bbox_inches='tight')
self.chart_paths.append(path)
return path
def _add_data_table(self, df):
"""Add data table to PDF."""
self.elements.append(Paragraph("Detailed Indicator Data", self.styles['Heading1']))
# Prepare table data
columns = ['indicator_name', 'target', 'actual', 'achievement', 'status']
available_cols = [c for c in columns if c in df.columns]
table_data = [[c.replace('_', ' ').title() for c in available_cols]]
for _, row in df.iterrows():
row_data = []
for col in available_cols:
val = row[col]
if col == 'achievement' and isinstance(val, (int, float)):
row_data.append(f"{val:.1f}%")
else:
row_data.append(str(val))
table_data.append(row_data)
# Create table
col_widths = [4*cm] + [2.5*cm] * (len(available_cols) - 1)
table = Table(table_data, colWidths=col_widths, repeatRows=1)
# Style table
style = [
('BACKGROUND', (0, 0), (-1, 0), self.config.COLORS['secondary']),
('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke),
('ALIGN', (0, 0), (-1, 0), 'CENTER'),
('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
('FONTSIZE', (0, 0), (-1, 0), 10),
('BOTTOMPADDING', (0, 0), (-1, 0), 8),
('BACKGROUND', (0, 1), (-1, -1), self.config.COLORS['light']),
('GRID', (0, 0), (-1, -1), 0.5, self.config.COLORS['border']),
('FONTNAME', (0, 1), (-1, -1), 'Helvetica'),
('FONTSIZE', (0, 1), (-1, -1), 9),
('PADDING', (0, 0), (-1, -1), 4),
('VALIGN', (0, 0), (-1, -1), 'MIDDLE'),
]
table.setStyle(TableStyle(style))
self.elements.append(table)
self.elements.append(Spacer(1, 20))
self.elements.append(PageBreak())
def _add_recommendations(self, df):
"""Add recommendations section."""
self.elements.append(Paragraph("Key Recommendations", self.styles['Heading1']))
recommendations = self._generate_recommendations(df)
for i, rec in enumerate(recommendations, 1):
bullet = f"{i}. {rec}"
self.elements.append(Paragraph(bullet, self.styles['BodyText']))
self.elements.append(Spacer(1, 20))
def _generate_recommendations(self, df):
"""Generate recommendations based on data."""
recs = []
if 'status' in df.columns:
off_track = df[df['status'] == 'off_track']
if len(off_track) > 0:
recs.append(f"Prioritize interventions for {len(off_track)} off-track indicators")
at_risk = df[df['status'] == 'at_risk']
if len(at_risk) > 0:
recs.append(f"Provide additional support to {len(at_risk)} at-risk indicators")
if 'achievement' in df.columns and df['achievement'].mean() < 80:
recs.append("Review program strategies to improve overall achievement")
recs.append("Continue regular monitoring and data quality checks")
recs.append("Schedule follow-up review in 30 days")
return recs[:5]
def _add_footer(self):
"""Add footer to PDF."""
footer_text = f"Report generated by M&E AI Agent • Page {self.elements.count(PageBreak) + 1}"
self.elements.append(Paragraph(footer_text, self.styles['Normal']))
def cleanup_charts(self):
"""Clean up temporary chart files."""
for path in self.chart_paths:
try:
os.remove(path)
except OSError:
pass
self.chart_paths = []Part 3: Complete Automated Reporting System with PDF
import pandas as pd
import os
from datetime import datetime
class AutomatedPDFReportSystem:
"""Complete system for automated PDF report generation."""
def __init__(self, data_source='excel', file_path='indicator_data.xlsx', output_dir='reports'):
self.data_source = data_source
self.file_path = file_path
self.output_dir = output_dir
self.df = None
self.pdf_generator = PDFReportGenerator(output_dir=output_dir)
def load_data(self):
"""Load and prepare data."""
if not os.path.exists(self.file_path):
raise FileNotFoundError(f"Data file not found: {self.file_path}")
self.df = pd.read_excel(self.file_path)
self._prepare_data()
return self.df
def _prepare_data(self):
"""Clean and prepare data."""
if 'date' in self.df.columns:
self.df['date'] = pd.to_datetime(self.df['date'])
if 'target' in self.df.columns and 'actual' in self.df.columns:
self.df['achievement'] = (self.df['actual'] / self.df['target'] * 100).round(1)
# Ensure status column exists
if 'status' not in self.df.columns:
self.df['status'] = 'on_track' # Default status
return self.df
def calculate_metrics(self):
"""Calculate performance metrics."""
total = len(self.df)
on_track = len(self.df[self.df['status'] == 'on_track']) if 'status' in self.df else 0
at_risk = len(self.df[self.df['status'] == 'at_risk']) if 'status' in self.df else 0
off_track = len(self.df[self.df['status'] == 'off_track']) if 'status' in self.df else 0
avg_achievement = self.df['achievement'].mean() if 'achievement' in self.df else 0
return {
'total_indicators': total,
'on_track': on_track,
'at_risk': at_risk,
'off_track': off_track,
'avg_achievement': avg_achievement
}
def generate_report(self, title=None, subtitle=None, include_charts=True):
"""Generate PDF report."""
if self.df is None:
self.load_data()
metrics = self.calculate_metrics()
# Set default title
if not title:
title = f"M&E Performance Report - {datetime.now().strftime('%B %Y')}"
if not subtitle:
subtitle = f"Generated on {datetime.now().strftime('%B %d, %Y at %H:%M')}"
# Generate PDF
filename = self.pdf_generator.generate_report(
df=self.df,
metrics=metrics,
title=title,
subtitle=subtitle,
include_charts=include_charts
)
# Clean up chart images
self.pdf_generator.cleanup_charts()
return filename
def generate_multiple(self, periods=None):
"""Generate reports for multiple periods."""
if not periods:
periods = ['weekly', 'monthly']
reports = {}
for period in periods:
# Filter data by period
period_df = self._filter_by_period(period)
if not period_df.empty:
title = f"{period.capitalize()} M&E Report - {datetime.now().strftime('%B %Y')}"
reports[period] = self.generate_report(
title=title,
subtitle=f"{period.capitalize()} Performance Overview"
)
return reports
def _filter_by_period(self, period):
"""Filter data by time period."""
if 'date' not in self.df.columns:
return self.df
today = datetime.now()
if period == 'weekly':
cutoff = today - pd.Timedelta(days=7)
elif period == 'monthly':
cutoff = today - pd.Timedelta(days=30)
else:
return self.df
return self.df[self.df['date'] >= cutoff]Part 4: Complete Usage Example
# main_pdf_report.py
import pandas as pd
from datetime import datetime
def main():
# Step 1: Create sample data with multiple dates
sample_data = []
dates = ['2024-01-15', '2024-02-15', '2024-03-15', '2024-04-15']
indicators = [
('Children Vaccinated', 5000, 'Health'),
('Teachers Trained', 200, 'Education'),
('Schools Reached', 100, 'Education'),
('Community Events', 50, 'Community'),
('Health Centers', 30, 'Health'),
('Water Access', 80, 'WASH')
]
for date in dates:
for name, target, category in indicators:
# Simulate varying actual values
import random
actual = target * (0.85 + random.uniform(0, 0.15))
status = 'on_track' if actual >= target * 0.9 else 'at_risk' if actual >= target * 0.7 else 'off_track'
sample_data.append({
'indicator_name': name,
'target': target,
'actual': round(actual),
'date': date,
'status': status,
'category': category
})
df = pd.DataFrame(sample_data)
df.to_excel('indicator_data.xlsx', index=False)
# Step 2: Initialize the PDF report system
system = AutomatedPDFReportSystem('excel', 'indicator_data.xlsx', 'pdf_reports')
# Step 3: Load data
system.load_data()
print(f"Loaded {len(system.df)} records")
# Step 4: Generate PDF report
print("\nGenerating PDF report...")
filename = system.generate_report(
title="Quarterly M&E Performance Report",
subtitle=f"Reporting Period: January - March 2024",
include_charts=True
)
print(f"\nReport generated successfully!")
print(f"PDF saved to: {filename}")
# Step 5: Generate weekly and monthly reports
print("\nGenerating period reports...")
reports = system.generate_multiple(['weekly', 'monthly'])
for period, file in reports.items():
print(f"{period.capitalize()} report: {file}")
if __name__ == "__main__":
main()
Troubleshooting PDF Generation
| Issue | Solution |
|---|---|
| Image not found | Check chart generation paths and file permissions |
| Font not rendering | Use Helvetica/Helvetica-Bold (built-in reportlab fonts) |
| Table overflow | Adjust colWidths or reduce font size |
| Memory error | Reduce number of charts or clean up temp files |
| Invalid page break | Use PageBreak() between major sections |
Best Practices for PDF Reports
- Use Consistent Branding: Apply organization colors and logos
- Optimize Chart Resolution: Use 150-200 DPI for good quality with manageable file size
- Add Page Numbers: Include footer with page numbering
- Include Metadata: Add document title, author, and creation date
- Test with Different Viewers: Verify PDF opens in Adobe, Chrome, and Edge
- Compress Final PDF: Use PDF compression tools for email distribution
Next Steps: Advanced PDF Features
- Custom Cover Pages: Add branded cover with logo and title
- Table of Contents: Auto-generate TOC with page numbers
- Watermarking: Add draft or confidential watermarks
- Multi-Language Support: Generate reports in multiple languages
- Digital Signatures: Add digital signature capabilities
- Batch Generation: Generate reports for multiple programs automatically
Master PDF Report Generation
The AI Agents for Evaluators Certificate teaches you to build complete M&E solutions including automated PDF reports, email delivery, and stakeholder management.
What you will learn:
- Build automated PDF report generation pipelines
- Create professional reports with embedded charts
- Customize formatting and styling
- Generate multi-format reports (PDF, HTML, Email)
Course Features: 32 lectures · Lifetime access · Certificate included · 241 students enrolled
Enroll Now – $249 Lifetime Access
Bundle with AI in M&E course and save 30%
This guide is part of the AI Agents for Evaluators series. Continue your learning journey with the full certificate course above.
