Excel and document tasks

Build a monthly summary with pandas groupby and check the totals

Summarize order-level CSV data by month with pandas, then reconcile the grouped counts, units, and amounts against the source. A small synthetic example makes every expected result easy to verify by hand.

Show contents

Who this is forThis guide is for people who need a repeatable monthly CSV summary and want explicit checks that aggregation did not lose records.

What you need
  • Python 3.12 and a terminal command that starts that version.
  • pandas installed with python -m pip install pandas.
  • A working folder where the script can create a new folder beneath outputs.
  • The source CSV must contain order_id, order_date, units, and amount columns.

01Define the monthly measures

This example produces one row per calendar month with three measures: order count, total units, and total amount. The synthetic source uses one row per order, so counting rows is equivalent to counting orders in this specific dataset.

The important step is reconciliation. After groupby, the monthly values are added again and compared with the source totals. A summary can look reasonable even when rows were accidentally filtered, duplicated, or dropped.

02Create the synthetic CSV

The following dataset is synthetic and was written specifically for this article. Save it as monthly_orders.csv. It contains seven orders across January, February, and March 2026.

csv
order_id,order_date,team,units,amount
O001,2026-01-05,North,2,40
O002,2026-01-20,South,3,60
O003,2026-02-02,North,1,25
O004,2026-02-18,South,4,100
O005,2026-02-28,North,2,50
O006,2026-03-03,South,5,125
O007,2026-03-15,North,1,30

The source totals are simple enough to check manually. There are 7 orders. Units total 18: 2 + 3 + 1 + 4 + 2 + 5 + 1. Amount totals 430: 40 + 60 + 25 + 100 + 50 + 125 + 30.

MeasureSource total
Orders7
Units18
Amount430

03Calculate the expected monthly rows by hand

January contains O001 and O002. February contains O003, O004, and O005. March contains O006 and O007. Adding each group gives the expected monthly table.

monthordersunitsamount
2026-0125100
2026-0237175
2026-0326155

The monthly totals reconcile with the source: 2 + 3 + 2 = 7 orders, 5 + 7 + 6 = 18 units, and 100 + 175 + 155 = 430 amount.

04Build and verify the summary with pandas

Save the following script as monthly_summary_pandas.py. It validates required columns, rejects missing or duplicate order IDs, parses dates with one explicit format, converts numeric columns, groups by month, reconciles the totals, and writes the result only after those checks pass.

python
from pathlib import Path
import pandas as pd

SOURCE = Path("monthly_orders.csv")
OUTPUT_DIR = Path("outputs") / "monthly_summary_result"
OUTPUT = OUTPUT_DIR / "monthly_summary.csv"
REQUIRED = ["order_id", "order_date", "units", "amount"]


def main() -> None:
    if not SOURCE.is_file():
        raise FileNotFoundError(f"Source CSV not found: {SOURCE}")
    if OUTPUT_DIR.exists():
        raise FileExistsError(f"Output folder already exists: {OUTPUT_DIR}")

    data = pd.read_csv(SOURCE, dtype={"order_id": "string"})

    missing = [name for name in REQUIRED if name not in data.columns]
    if missing:
        raise ValueError(f"Missing required columns: {missing}")
    if data.empty:
        raise ValueError("Source CSV contains no data rows.")
    if data[REQUIRED].isna().any().any():
        raise ValueError("A required field contains a missing value.")
    if data["order_id"].str.strip().eq("").any():
        raise ValueError("order_id contains a blank value.")
    if data["order_id"].duplicated().any():
        raise ValueError("Duplicate order_id found.")

    data["order_date"] = pd.to_datetime(
        data["order_date"], format="%Y-%m-%d", errors="raise"
    )
    data["units"] = pd.to_numeric(data["units"], errors="raise")
    data["amount"] = pd.to_numeric(data["amount"], errors="raise")
    data["month"] = data["order_date"].dt.to_period("M").astype(str)

    summary = (
        data.groupby("month", as_index=False, sort=True)
        .agg(
            orders=("order_id", "count"),
            units=("units", "sum"),
            amount=("amount", "sum"),
        )
    )

    source_totals = (len(data), data["units"].sum(), data["amount"].sum())
    summary_totals = (
        summary["orders"].sum(),
        summary["units"].sum(),
        summary["amount"].sum(),
    )
    if summary_totals != source_totals:
        raise RuntimeError("Monthly totals do not reconcile with the source.")

    OUTPUT_DIR.parent.mkdir(parents=True, exist_ok=True)
    OUTPUT_DIR.mkdir()
    summary.to_csv(OUTPUT, index=False)

    check = pd.read_csv(OUTPUT)
    if check.to_dict("records") != summary.to_dict("records"):
        raise RuntimeError("Saved CSV does not match the calculated summary.")

    print(f"Source rows: {len(data)}.")
    print(f"Months: {len(summary)}.")
    print(f"Verified totals: units={source_totals[1]}, amount={source_totals[2]}.")
    print(f"Output: {OUTPUT.as_posix()}")


if __name__ == "__main__":
    main()
text
python monthly_summary_pandas.py

05Compare the expected output

The output should contain three data rows sorted by month. The expected CSV contents are shown below.

csv
month,orders,units,amount
2026-01,2,5,100
2026-02,3,7,175
2026-03,2,6,155

The expected console output below was derived by hand from the synthetic data and code. It is not a captured execution log.

text
Source rows: 7.
Months: 3.
Verified totals: units=18, amount=430.
Output: outputs/monthly_summary_result/monthly_summary.csv

06Reconcile the result and recognize common errors

  1. Count the source rows: 7.
  2. Add monthly orders: 2 + 3 + 2 = 7.
  3. Add monthly units: 5 + 7 + 6 = 18.
  4. Add monthly amount: 100 + 175 + 155 = 430.
  5. Confirm that the output contains exactly 2026-01, 2026-02, and 2026-03.
  6. Run the script again without changing OUTPUT_DIR. It should stop with FileExistsError instead of replacing the existing result.
SymptomWhat to check
ModuleNotFoundError for pandasInstall pandas into the same Python environment with python -m pip install pandas.
Missing required columnsCheck the CSV header before changing the expected schema.
Date parsing errorConfirm that every order_date uses YYYY-MM-DD.
Numeric conversion errorInspect units and amount for text, currency symbols, or separators that require an explicit cleanup rule.
Duplicate order_id foundDetermine whether the repeated ID is an accidental duplicate or represents a different data model.
FileExistsErrorReview the previous result and use a fresh output folder rather than overwriting it.

Matching grand totals do not prove that every month is correct because two monthly errors can offset each other. For important reports, also inspect several known source rows and month-level subtotals.

07Understand the limits

The example assumes one row per order and whole-number amounts. If one order spans several line-item rows, use a rule such as order_id.nunique() instead of row count. Real monetary data may also need fixed-point integer units or Decimal-based processing instead of ordinary floating-point arithmetic.

Month is derived directly from a date with no time-zone handling. If real data contains timestamps from several zones, define the business time zone before grouping. Refunds, cancellations, filters, and missing records also need explicit business rules; arithmetic reconciliation alone cannot determine whether those rows belong in the report.

Execution and verification record

ChatGPT Python tool · Python 3.13.5 · pandas 2.2.3

  • The main run printed exactly: 'Source rows: 7.', 'Months: 3.', 'Verified totals: units=18, amount=430.', and 'Output: outputs/monthly_summary_result/monthly_summary.csv'.
  • The generated CSV contents exactly matched the article: 2026-01,2,5,100; 2026-02,3,7,175; 2026-03,2,6,155.
  • The monthly order counts reconciled to 7: 2 + 3 + 2.
  • The monthly units reconciled to 18: 5 + 7 + 6.
  • The monthly amount reconciled to 430: 100 + 175 + 155.
  • The output contained exactly 3 data rows sorted by month.
  • The SHA-256 hash of monthly_orders.csv was unchanged after processing, confirming the source file was not modified.
  • A second run with outputs/monthly_summary_result already present raised FileExistsError before overwriting the previous result.
Verification limits
  • Duplicate-ID rejection, missing required values, invalid dates, nonnumeric units or amounts, floating-point monetary data, time zones, refunds, cancellations, line-item order models, permission failures, partial-write failures, and very large datasets were not separately stress-tested.
  • The Python environment emitted an unrelated artifact-tool spreadsheet warmup warning on stderr, but the article script itself completed successfully with exit code 0 and produced the expected output.

References

The explanations and examples were written for this site. See the official sources below for the related behavior and concepts.