Learn how to turn raw transaction data into a professional, multi-sheet Excel workbook using pandas pivot_table and openpyxl. This lesson covers building multiple pivot summaries, applying header styles and number formats, adding a cover sheet, and structuring the whole pipeline as a reusable script.

You've done the analysis. The numbers are solid. Now someone in leadership wants "a report" — ideally something they can open in Excel, filter, and forward to their team. If your current workflow involves running Python, copying results into a spreadsheet, and then manually formatting it, you're doing three jobs that one script should handle.
This lesson is about closing that loop. We're going to take a realistic sales dataset, build several meaningful summary tables using pivot_table, and then write all of them into a properly formatted, multi-sheet Excel workbook using to_excel and openpyxl. The finished product will look like something a business analyst put together deliberately — not a data dump.
By the end of this lesson, you'll have a repeatable script you can adapt to any reporting scenario. Run it against a new month of data and you get a new report. That's the goal.
What you'll learn:
pivot_table works and how to configure it for real-world aggregation taskspd.ExcelWriteropenpyxlYou should be comfortable with pandas DataFrames — loading data, selecting columns, filtering rows. If you're newer to pandas, start with Your First pandas DataFrame: Loading CSV and Excel Files and Exploring Data and Selecting and Filtering Data in pandas: loc, iloc, and Boolean Masks first.
You should also have a working environment with pandas, openpyxl, and Jupyter or a Python script runner set up. If you need that foundation, see Setting Up Python for Data Analysis: Python, VS Code, Jupyter, and Virtual Environments.
Install dependencies if you haven't already:
pip install pandas openpyxl
Rather than load a CSV from an unknown path, let's construct a realistic dataset in code so you can follow along immediately. This is a transaction-level sales dataset — the kind you'd get from a CRM or ERP export.
import pandas as pd
import numpy as np
from datetime import date, timedelta
import random
random.seed(42)
np.random.seed(42)
regions = ["Northeast", "Southeast", "Midwest", "West"]
categories = ["Hardware", "Software", "Services", "Training"]
reps = {
"Northeast": ["Alice Marsh", "Brian Cho"],
"Southeast": ["Carlos Vega", "Diana Pham"],
"Midwest": ["Ethan Brooks", "Fiona Chen"],
"West": ["George Kim", "Hannah Lee"],
}
n = 800
start = date(2024, 1, 1)
records = []
for _ in range(n):
region = random.choice(regions)
rep = random.choice(reps[region])
category = random.choice(categories)
qty = random.randint(1, 50)
unit_price = round(random.uniform(200, 5000), 2)
revenue = round(qty * unit_price, 2)
cost = round(revenue * random.uniform(0.4, 0.7), 2)
order_date = start + timedelta(days=random.randint(0, 364))
records.append({
"order_date": order_date,
"region": region,
"rep": rep,
"category": category,
"quantity": qty,
"unit_price": unit_price,
"revenue": revenue,
"cost": cost,
"profit": round(revenue - cost, 2),
})
df = pd.DataFrame(records)
df["order_date"] = pd.to_datetime(df["order_date"])
df["month"] = df["order_date"].dt.to_period("M").astype(str)
df["quarter"] = df["order_date"].dt.to_period("Q").astype(str)
print(df.shape)
print(df.dtypes)
print(df.head(3))
You should see 800 rows with clean numeric columns and date-derived period columns. This is the raw material for every pivot we're about to build.
Note
In a real project, you'd load this data from a CSV, database, or Excel file. The structure here — one row per transaction with dimension columns (region, category, rep) and measure columns (revenue, cost, profit) — is exactly the shape pivot_table expects. If your data needs cleaning first, check Cleaning Messy Data with pandas: Missing Values, Duplicates, and Data Types.
pivot_table is pandas' version of Excel's PivotTable — and if you've used that feature, the mental model translates almost exactly. You specify what goes on the rows (index), what goes on the columns (columns), what values to aggregate (values), and how to aggregate them (aggfunc).
Here's the minimal signature:
pd.pivot_table(
data, # your DataFrame
values=..., # column(s) to aggregate
index=..., # row grouping field(s)
columns=..., # column grouping field (optional)
aggfunc=..., # function or dict of functions
fill_value=0, # replace NaN with this (often 0)
margins=False, # add row/column totals
)
If you want a deeper dive into the mechanics — including stacked indexes and multi-level output — see Reshaping Data with pivot_table, melt, and stack in pandas. For this lesson, we'll use pivot_table in its practical, report-building mode and focus on what makes output useful to a business audience.
Let's start with the most natural summary: revenue by region and category.
pt_region_category = pd.pivot_table(
df,
values=["revenue", "profit"],
index="region",
columns="category",
aggfunc="sum",
fill_value=0,
margins=True,
margins_name="Total",
)
print(pt_region_category)
This gives you a multi-level column structure: the top level is the measure (revenue, profit), the second level is the category. That's useful for analysis in Python, but when you write it to Excel you'll want flat column names. We'll handle that shortly.
Key insight
margins=True adds a "Total" row and column automatically — the equivalent of Excel's Grand Total option. Use margins_name="Total" to control the label, since the default is "All" which reads awkwardly in a business report.
A useful workbook usually has several angles on the same data. We'll build four pivot tables, each telling a different story.
pt1 = pd.pivot_table(
df,
values=["revenue", "profit"],
index="region",
columns="category",
aggfunc="sum",
fill_value=0,
margins=True,
margins_name="Total",
)
# Flatten multi-level columns
pt1.columns = [f"{measure} — {cat}" for measure, cat in pt1.columns]
pt1 = pt1.reset_index()
pt1 = pt1.rename(columns={"region": "Region"})
pt2 = pd.pivot_table(
df,
values="revenue",
index="quarter",
aggfunc=["sum", "mean", "count"],
fill_value=0,
)
pt2.columns = ["Total Revenue", "Avg Order Value", "Order Count"]
pt2 = pt2.reset_index()
pt2 = pt2.rename(columns={"quarter": "Quarter"})
pt2["Total Revenue"] = pt2["Total Revenue"].round(2)
pt2["Avg Order Value"] = pt2["Avg Order Value"].round(2)
pt3 = pd.pivot_table(
df,
values=["revenue", "profit", "quantity"],
index=["region", "rep"],
aggfunc={
"revenue": "sum",
"profit": "sum",
"quantity": "sum",
},
fill_value=0,
margins=True,
margins_name="Total",
)
pt3["margin_pct"] = (pt3["profit"] / pt3["revenue"] * 100).round(1)
pt3 = pt3.reset_index()
pt3 = pt3.rename(columns={
"region": "Region",
"rep": "Sales Rep",
"revenue": "Revenue",
"profit": "Profit",
"quantity": "Units Sold",
"margin_pct": "Margin %",
})
Notice we computed margin_pct after the pivot, not before. You generally can't ask pivot_table to compute a ratio of aggregates in one step — but it's straightforward to add derived columns after the fact.
pt4 = pd.pivot_table(
df,
values=["revenue", "cost", "profit"],
index="category",
aggfunc={
"revenue": ["sum", "mean"],
"cost": "sum",
"profit": "sum",
},
fill_value=0,
margins=True,
margins_name="Total",
)
# Flatten and rename
pt4.columns = [f"{col[0].title()} ({col[1].title()})" for col in pt4.columns]
pt4 = pt4.reset_index()
pt4 = pt4.rename(columns={"category": "Category"})
Tip
After any pivot_table call, always print .dtypes and .head() before writing to Excel. Multi-level column indexes, residual Period types, or unexpected object columns will cause formatting problems downstream. Better to catch them here than to debug a corrupted workbook.
The core pattern for multi-sheet workbooks is pd.ExcelWriter used as a context manager. Each DataFrame gets written to a named sheet with df.to_excel(writer, sheet_name=...). When the context manager exits, the file is saved.
output_path = "sales_summary_report.xlsx"
with pd.ExcelWriter(output_path, engine="openpyxl") as writer:
pt1.to_excel(writer, sheet_name="Region × Category", index=False)
pt2.to_excel(writer, sheet_name="Quarterly Trend", index=False)
pt3.to_excel(writer, sheet_name="Rep Performance", index=False)
pt4.to_excel(writer, sheet_name="Category Detail", index=False)
print(f"Workbook written to {output_path}")
Open that file. You'll get four sheets with data, but the presentation is rough: columns are too narrow, numbers lack formatting, headers look like everything else. That's what we fix next.
Warning
If you're used to calling writer.save() at the end, note that it was deprecated in pandas 1.5 and removed in 2.0. The context manager (with ... as writer:) handles saving automatically. Using the old pattern on a modern pandas install will raise an AttributeError.
After to_excel runs, the workbook object is still live inside the ExcelWriter context. You can grab it via writer.book and then access individual sheets via writer.sheets. This is where openpyxl takes over.
Let's build a helper function that handles the most important formatting concerns for any sheet: column widths, header style, and number formats.
from openpyxl.styles import Font, PatternFill, Alignment, Border, Side
from openpyxl.utils import get_column_letter
def format_sheet(ws, df, currency_cols=None, pct_cols=None, int_cols=None):
"""
Apply professional formatting to an openpyxl worksheet.
Parameters
----------
ws : openpyxl Worksheet object
df : the DataFrame that was written to this sheet (used for column metadata)
currency_cols : list of column names to format as currency
pct_cols : list of column names to format as percentage (0–100 scale, e.g. "45.2")
int_cols : list of column names to format as integers with comma separator
"""
currency_cols = currency_cols or []
pct_cols = pct_cols or []
int_cols = int_cols or []
# --- Header row styling ---
header_fill = PatternFill(start_color="1F3864", end_color="1F3864", fill_type="solid")
header_font = Font(bold=True, color="FFFFFF", size=11)
header_align = Alignment(horizontal="center", vertical="center", wrap_text=True)
thin = Side(style="thin", color="CCCCCC")
cell_border = Border(bottom=thin)
for cell in ws[1]:
cell.fill = header_fill
cell.font = header_font
cell.alignment = header_align
# --- Column widths and number formats ---
col_name_to_idx = {col: i + 1 for i, col in enumerate(df.columns)}
for col_name, col_idx in col_name_to_idx.items():
col_letter = get_column_letter(col_idx)
# Auto-width: find max content length in column
max_len = len(str(col_name))
for row in ws.iter_rows(min_row=2, min_col=col_idx, max_col=col_idx):
for cell in row:
if cell.value is not None:
max_len = max(max_len, len(str(cell.value)))
ws.column_dimensions[col_letter].width = min(max_len + 4, 40)
# Number formats
if col_name in currency_cols:
for row in ws.iter_rows(min_row=2, min_col=col_idx, max_col=col_idx):
for cell in row:
cell.number_format = '"$"#,##0.00'
cell.alignment = Alignment(horizontal="right")
elif col_name in pct_cols:
for row in ws.iter_rows(min_row=2, min_col=col_idx, max_col=col_idx):
for cell in row:
cell.number_format = '0.0"%"'
cell.alignment = Alignment(horizontal="right")
elif col_name in int_cols:
for row in ws.iter_rows(min_row=2, min_col=col_idx, max_col=col_idx):
for cell in row:
cell.number_format = "#,##0"
cell.alignment = Alignment(horizontal="right")
# --- Zebra striping for readability ---
stripe_fill = PatternFill(start_color="EEF2F7", end_color="EEF2F7", fill_type="solid")
for i, row in enumerate(ws.iter_rows(min_row=2), start=2):
if i % 2 == 0:
for cell in row:
if cell.fill.start_color.rgb == "00000000": # only uncolored cells
cell.fill = stripe_fill
# --- Freeze the header row ---
ws.freeze_panes = "A2"
This function is doing a lot of practical work: it styles the header with a dark blue background and white bold text (a clean, professional default), auto-sizes columns based on actual content, applies appropriate number formats, adds zebra striping for readability, and freezes the top row.
Key insight
The number_format strings in openpyxl use Excel's own format codes. '"$"#,##0.00' renders as dollar amounts with commas and two decimal places. '0.0"%"' appends a literal percent sign without dividing by 100 — useful when your values are already on a 0–100 scale like our Margin % column.
Now we tie everything together. The full script builds the four pivot tables and writes a properly formatted workbook in one pass.
import pandas as pd
import numpy as np
from openpyxl.styles import Font, PatternFill, Alignment
from openpyxl.utils import get_column_letter
# --- [Assume df is already built as above] ---
def build_pivot_tables(df):
"""Return a dict of named DataFrames ready to write to Excel."""
# Table 1: Region × Category
pt1 = pd.pivot_table(
df, values=["revenue", "profit"],
index="region", columns="category",
aggfunc="sum", fill_value=0,
margins=True, margins_name="Total",
)
pt1.columns = [f"{m.title()} — {c}" for m, c in pt1.columns]
pt1 = pt1.reset_index().rename(columns={"region": "Region"})
# Table 2: Quarterly Trend
pt2 = pd.pivot_table(
df, values="revenue",
index="quarter",
aggfunc=["sum", "mean", "count"],
fill_value=0,
)
pt2.columns = ["Total Revenue", "Avg Order Value", "Order Count"]
pt2 = pt2.reset_index().rename(columns={"quarter": "Quarter"})
pt2["Total Revenue"] = pt2["Total Revenue"].round(2)
pt2["Avg Order Value"] = pt2["Avg Order Value"].round(2)
# Table 3: Rep Performance
pt3 = pd.pivot_table(
df, values=["revenue", "profit", "quantity"],
index=["region", "rep"],
aggfunc={"revenue": "sum", "profit": "sum", "quantity": "sum"},
fill_value=0, margins=True, margins_name="Total",
)
pt3["margin_pct"] = (pt3["profit"] / pt3["revenue"] * 100).round(1)
pt3 = pt3.reset_index().rename(columns={
"region": "Region", "rep": "Sales Rep",
"revenue": "Revenue", "profit": "Profit",
"quantity": "Units Sold", "margin_pct": "Margin %",
})
# Table 4: Category Detail
pt4 = pd.pivot_table(
df, values=["revenue", "cost", "profit"],
index="category",
aggfunc={"revenue": ["sum", "mean"], "cost": "sum", "profit": "sum"},
fill_value=0, margins=True, margins_name="Total",
)
pt4.columns = [f"{c[0].title()} ({c[1].title()})" for c in pt4.columns]
pt4 = pt4.reset_index().rename(columns={"category": "Category"})
return {
"Region × Category": pt1,
"Quarterly Trend": pt2,
"Rep Performance": pt3,
"Category Detail": pt4,
}
def write_report(tables, output_path):
"""Write all tables to a formatted multi-sheet Excel workbook."""
currency_map = {
"Region × Category": [c for c in tables["Region × Category"].columns if "Revenue" in c or "Profit" in c],
"Quarterly Trend": ["Total Revenue", "Avg Order Value"],
"Rep Performance": ["Revenue", "Profit"],
"Category Detail": [c for c in tables["Category Detail"].columns if "Revenue" in c or "Profit" in c or "Cost" in c],
}
pct_map = {
"Rep Performance": ["Margin %"],
}
int_map = {
"Quarterly Trend": ["Order Count"],
"Rep Performance": ["Units Sold"],
}
with pd.ExcelWriter(output_path, engine="openpyxl") as writer:
for sheet_name, df in tables.items():
df.to_excel(writer, sheet_name=sheet_name, index=False)
ws = writer.sheets[sheet_name]
format_sheet(
ws, df,
currency_cols=currency_map.get(sheet_name, []),
pct_cols=pct_map.get(sheet_name, []),
int_cols=int_map.get(sheet_name, []),
)
print(f"Report written: {output_path}")
# --- Run it ---
tables = build_pivot_tables(df)
write_report(tables, "sales_summary_report.xlsx")
Open the workbook. Each sheet should have a dark header row, appropriately sized columns, dollar signs on revenue figures, a percent sign on margin, and alternating row shading. That's a report someone can use without touching it.
Tip
If you want to add an auto-filter to every sheet (so users can slice data like a real PivotTable), add ws.auto_filter.ref = ws.dimensions inside the format_sheet function after the freeze panes line. ws.dimensions returns the full range of the sheet as a string like "A1:J25".
A professional workbook usually has a cover or summary sheet — the first thing people see when they open the file. Here's how to prepend one programmatically.
from openpyxl import load_workbook
from openpyxl.styles import Font, Alignment
import datetime
def add_cover_sheet(output_path, report_title, summary_stats):
"""
Add a cover sheet as the first sheet in the workbook.
summary_stats: dict of {label: value} pairs to display
"""
wb = load_workbook(output_path)
# Insert a new sheet at position 0
ws = wb.create_sheet("Summary", 0)
# Title
ws["B2"] = report_title
ws["B2"].font = Font(bold=True, size=18, color="1F3864")
ws["B2"].alignment = Alignment(horizontal="left", vertical="center")
# Generated date
ws["B3"] = f"Generated: {datetime.date.today().strftime('%B %d, %Y')}"
ws["B3"].font = Font(italic=True, size=11, color="666666")
# Stats table
ws["B5"] = "Metric"
ws["C5"] = "Value"
for cell in [ws["B5"], ws["C5"]]:
cell.font = Font(bold=True, color="FFFFFF")
cell.fill = PatternFill(start_color="1F3864", end_color="1F3864", fill_type="solid")
for i, (label, value) in enumerate(summary_stats.items(), start=6):
ws[f"B{i}"] = label
ws[f"C{i}"] = value
ws[f"C{i}"].alignment = Alignment(horizontal="right")
ws.column_dimensions["A"].width = 3 # left margin
ws.column_dimensions["B"].width = 28
ws.column_dimensions["C"].width = 20
ws.sheet_view.showGridLines = False # hide gridlines for cleaner look
wb.save(output_path)
# Compute summary stats from df
summary = {
"Total Revenue": f"${df['revenue'].sum():,.0f}",
"Total Profit": f"${df['profit'].sum():,.0f}",
"Overall Margin": f"{df['profit'].sum() / df['revenue'].sum() * 100:.1f}%",
"Total Orders": f"{len(df):,}",
"Date Range": f"{df['order_date'].min().date()} to {df['order_date'].max().date()}",
"Regions": ", ".join(sorted(df["region"].unique())),
}
add_cover_sheet(
"sales_summary_report.xlsx",
"Annual Sales Performance Report — 2024",
summary,
)
Notice that we're using load_workbook to reopen the file after the ExcelWriter context has closed. This is intentional: ExcelWriter in openpyxl mode doesn't support inserting sheets at specific positions as gracefully as opening the workbook directly. Load, modify, save — a clean pattern.
Note
Setting ws.sheet_view.showGridLines = False hides the worksheet grid, which makes cover sheets and dashboard-style sheets look much more polished. It has no effect on data sheets, but it's a nice touch on the summary page.
Now it's your turn. Take the dataset we built and extend the report with a new analysis.
Your task: Add a fifth sheet called "Monthly Heatmap" that shows monthly revenue broken out by region. Specifically:
month as the index and region as the columns, aggregating revenue with sum."Total" column that sums across all regions for each month."$"#,##0).openpyxl.formatting.rule.ColorScaleRule or DataBarRule for this — it's worth exploring the openpyxl docs.The pivot for step 1 looks like this to get you started:
pt_monthly = pd.pivot_table(
df,
values="revenue",
index="month",
columns="region",
aggfunc="sum",
fill_value=0,
)
pt_monthly["Total"] = pt_monthly.sum(axis=1)
pt_monthly = pt_monthly.reset_index().rename(columns={"month": "Month"})
Add it to the tables dict in build_pivot_tables and update the currency_map to include all numeric columns. Then regenerate the report.
If you write a pivot table with columns= specified and don't flatten the column index first, to_excel will write two header rows with merged cells — which looks odd and makes downstream filtering harder.
# Before writing, check for MultiIndex columns
print(type(pt1.columns)) # MultiIndex means you need to flatten
# Fix: join the levels into a single string
pt1.columns = ["_".join(col).strip() for col in pt1.columns.values]
When you add a derived column (like margin_pct) after a pivot that includes margins=True, the Total row will have a NaN margin percentage because 0 / 0 is undefined or the formula doesn't apply cleanly. Fix it manually:
# Recalculate the Total row's margin after joining
total_mask = pt3["Region"] == "Total"
pt3.loc[total_mask, "Margin %"] = (
pt3.loc[total_mask, "Profit"] / pt3.loc[total_mask, "Revenue"] * 100
).round(1)
Excel limits sheet names to 31 characters. If your sheet name is longer, openpyxl will raise a ValueError. Use [:31] to truncate if you're building sheet names dynamically.
This usually means openpyxl wrote something Excel doesn't recognize. The most common cause is writing a pandas Period dtype into a cell — Excel doesn't know what to do with it. Always convert Period columns to strings: df["quarter"].astype(str).
The formatting functions must run inside the pd.ExcelWriter context, after to_excel but before the context exits. If you try to format after the with block closes, the workbook is already saved and the writer.sheets reference is stale. Always follow the pattern: write → format → exit.
with pd.ExcelWriter(path, engine="openpyxl") as writer:
df.to_excel(writer, sheet_name="Data", index=False)
ws = writer.sheets["Data"] # ← grab the sheet reference here
format_sheet(ws, df, ...) # ← format it here, inside the context
# ← file saves here when the 'with' block exits
Some reports genuinely benefit from the multi-level column structure — especially when you're comparing the same measure across many categories side by side. For those cases, you'll want to understand how to work with MultiIndex columns and rows after groupby and pivot_table, which is covered in depth in Reshaping and Analyzing Multi-Level Data in pandas: Working with MultiIndex Columns and Rows After groupby and pivot_table.
For Excel output specifically, flat columns almost always win. Business users expect one header row they can filter and sort. Multi-level headers look great in Jupyter notebooks and confusing in Excel.
The golden rule: flatten before you write. Pick a separator that makes the column names readable. " — " works well (Revenue — Hardware, Profit — Hardware), and " | " is another option.
If this report runs monthly, you don't want to re-edit the script every time. The right pattern is to parameterize the date range and accept the data path as an argument. Here's the skeleton:
# report_builder.py
import argparse
import pandas as pd
from pathlib import Path
def load_data(source_path):
path = Path(source_path)
if path.suffix == ".csv":
return pd.read_csv(path, parse_dates=["order_date"])
elif path.suffix in (".xlsx", ".xls"):
return pd.read_excel(path, parse_dates=["order_date"])
else:
raise ValueError(f"Unsupported file type: {path.suffix}")
def run_report(source_path, output_path, start_date=None, end_date=None):
df = load_data(source_path)
if start_date:
df = df[df["order_date"] >= pd.to_datetime(start_date)]
if end_date:
df = df[df["order_date"] <= pd.to_datetime(end_date)]
df["month"] = df["order_date"].dt.to_period("M").astype(str)
df["quarter"] = df["order_date"].dt.to_period("Q").astype(str)
tables = build_pivot_tables(df)
write_report(tables, output_path)
add_cover_sheet(output_path, "Sales Performance Report", compute_summary(df))
print(f"Done. Report saved to {output_path}")
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("source", help="Path to input CSV or Excel file")
parser.add_argument("output", help="Path for output Excel workbook")
parser.add_argument("--start", help="Start date (YYYY-MM-DD)", default=None)
parser.add_argument("--end", help="End date (YYYY-MM-DD)", default=None)
args = parser.parse_args()
run_report(args.source, args.output, args.start, args.end)
Run it like:
python report_builder.py sales_2024.csv output/report_q4.xlsx --start 2024-10-01 --end 2024-12-31
This is the foundation of an automated reporting pipeline. If you want to take it further — scheduling this to run overnight or on a trigger — the next step is Building and Automating Recurring Reports with pandas: Scheduling Scripts to Run Without You.
You've built a complete reporting pipeline: raw transaction data in, formatted multi-sheet Excel workbook out. The core skills you've practiced:
pivot_table to build multiple angles of aggregation from a single DataFrame, including multi-function aggregation and computed columnspd.ExcelWriteropenpyxl inside the writer contextload_workbookA few directions to explore from here:
More formatting power: The Automating Excel Reports with pandas and openpyxl: Formatted Workbooks Without Manual Work lesson goes deeper on conditional formatting, charts embedded in the workbook, and more advanced openpyxl patterns.
Richer aggregations: If your summaries need weighted averages, percent-of-total columns, or custom rollup logic, Weighted Averages, Percent of Total, and Custom Aggregations in pandas: Going Beyond sum and mean in groupby covers the techniques that pivot_table alone can't handle.
Exporting to other formats: Sometimes stakeholders want CSV exports alongside Excel, or you need to post JSON to an API. Exporting and Sharing Analysis Results: Writing CSV, Excel, and JSON Files from pandas covers all three formats in one place.
The goal is always the same: analysis that's reproducible, reports that look deliberate, and workflows that don't require you to be in the room.
Python for Data Analysis
Auditing and Reconciling Data Across Sources in pandas: Matching Totals, Flagging Discrepancies, and Building a Reconciliation Report
Building a Multi-Source Reconciliation Report in pandas: Comparing Actuals vs. Targets, Flagging Variances, and Exporting a Stakeholder-Ready Excel Summary