समझिए
1. स्थिति
हर ब्रांच अपनी बिक्री की एक CSV भेजती है। महीने के आख़िर में ऐसा फ़ोल्डर मिलता है:
data/2026-09/
north.csv
south.csv
west.csv
हर फ़ाइल में है: Date, Salesperson, Region, Product, Amount। तारीख़ें भारतीय फ़ॉर्मेट (15/09/2026) में। कुछ रो में समस्याएँ हैं: अतिरिक्त स्पेस, छोटे अक्षरों में नाम, गलत तारीख़, Amount में "NA", एक डुप्लिकेट एंट्री।
2. स्क्रिप्ट क्या बनाती है
Sales_Report_2026-09.xlsx जिसमें ये शीट:
| शीट | क्या है |
|---|---|
| By Region | हर रीजन का टोटल, सबसे बड़ा पहले |
| By Salesperson | हर व्यक्ति के ऑर्डर और टोटल |
| Region x Product | पिवट टेबल |
| Clean Data | सारी सही रो, स्रोत फ़ाइल के नाम के साथ |
| Rejected Rows | गलत तारीख़ या रकम वाली रो, मूल टेक्स्ट के साथ, ताकि कोई ठीक कर सके |
हर शीट को सजा हेडर, नंबर और तारीख़ फ़ॉर्मेट, कॉलम चौड़ाई और जमी हुई हेडर रो मिलती है।
3. प्रोजेक्ट की बनावट
monthly-report/
.venv/
data/2026-09/*.csv
monthly_report.py
requirements.txt ← pandas, openpyxl
4. स्क्रिप्ट
"""
monthly_report.py
Combine all CSV files in a folder, clean them, summarise, and write a formatted Excel report.
Usage:
python monthly_report.py --input data/2026-09 --output Sales_Report_2026-09.xlsx
"""
import argparse
from pathlib import Path
import pandas as pd
from openpyxl import load_workbook
from openpyxl.styles import Font, PatternFill, Alignment
from openpyxl.utils import get_column_letter
TEXT_COLS = ["Salesperson", "Region", "Product"]
def load_data(folder: Path, files: list[Path] | None = None) -> pd.DataFrame:
files = sorted(folder.glob("*.csv")) if files is None else files
if not files:
raise FileNotFoundError(f"No CSV files found in {folder}")
frames = []
required = {"Date", "Salesperson", "Region", "Product", "Amount"}
for f in files:
frame = pd.read_csv(f, keep_default_na=False, dtype=str)
frame.columns = frame.columns.str.strip()
missing = required - set(frame.columns)
if missing:
raise ValueError(f"{f.name}: missing columns: {', '.join(sorted(missing))}")
frames.append(frame.assign(SourceFile=f.name))
return pd.concat(frames, ignore_index=True)
def clean(df: pd.DataFrame) -> tuple[pd.DataFrame, pd.DataFrame]:
df = df.copy()
df.columns = df.columns.str.strip()
dates = pd.to_datetime(df["Date"], format="%d/%m/%Y", errors="coerce")
amounts = pd.to_numeric(df["Amount"], errors="coerce")
bad = dates.isna() | amounts.isna() | amounts.isin([float("inf"), float("-inf")])
rejected = df[bad].copy() # keep the original text for review
for col in TEXT_COLS:
df[col] = df[col].astype(str).str.strip().str.title()
good = (df[~bad]
.assign(Date=dates[~bad], Amount=amounts[~bad])
.drop_duplicates(subset=["Date", "Salesperson", "Region", "Product", "Amount"]))
return good, rejected
def summarise(df: pd.DataFrame) -> dict[str, pd.DataFrame]:
by_region = (df.groupby("Region", as_index=False)["Amount"].sum()
.sort_values("Amount", ascending=False))
by_person = (df.groupby("Salesperson", as_index=False)
.agg(Orders=("Amount", "count"), Total=("Amount", "sum"))
.sort_values("Total", ascending=False))
pivot = df.pivot_table(index="Region", columns="Product", values="Amount",
aggfunc="sum", fill_value=0)
return {"By Region": by_region, "By Salesperson": by_person, "Region x Product": pivot}
def write_report(clean_df, rejected, summaries, out_path: Path) -> None:
out_path.parent.mkdir(parents=True, exist_ok=True)
with pd.ExcelWriter(out_path, engine="openpyxl") as writer:
for name, frame in summaries.items():
frame.to_excel(writer, sheet_name=name, index=(name == "Region x Product"))
clean_df.to_excel(writer, sheet_name="Clean Data", index=False)
rejected.to_excel(writer, sheet_name="Rejected Rows", index=False)
# CSV text must remain text, even when it starts with an equals sign.
for ws in writer.book.worksheets:
for row in ws:
for cell in row:
if isinstance(cell.value, str):
cell.data_type = "s"
def format_report(path: Path) -> None:
wb = load_workbook(path)
header_fill = PatternFill("solid", start_color="1F4E78")
for ws in wb.worksheets:
for cell in ws[1]:
cell.font = Font(bold=True, color="FFFFFF")
cell.fill = header_fill
cell.alignment = Alignment(horizontal="center")
for col_cells in ws.columns:
length = max(len(str(c.value)) if c.value is not None else 0 for c in col_cells)
ws.column_dimensions[get_column_letter(col_cells[0].column)].width = min(length + 3, 45)
for row in ws.iter_rows(min_row=2):
for c in row:
if isinstance(c.value, (int, float)):
c.number_format = "#,##0"
elif c.is_date:
c.number_format = "dd-mmm-yyyy"
ws.freeze_panes = "A2"
wb.save(path)
def main() -> None:
parser = argparse.ArgumentParser(description="Build the monthly sales report.")
parser.add_argument("--input", required=True, help="Folder containing CSV files")
parser.add_argument("--output", required=True, help="Excel file to create")
args = parser.parse_args()
raw = load_data(Path(args.input))
good, rejected = clean(raw)
if good.empty:
raise ValueError("No valid rows; input files retained for review")
summaries = summarise(good)
out = Path(args.output)
write_report(good, rejected, summaries, out)
format_report(out)
print(f"Rows read: {len(raw)} | clean: {len(good)} | rejected: {len(rejected)}")
print(f"Report saved: {out.resolve()}")
if __name__ == "__main__":
main()
5. यह कैसे व्यवस्थित है
- हर काम का एक फ़ंक्शन —
load_data,clean,summarise,write_report,format_report। एक हिस्से को जाँचना और बदलना आसान। cleanगलत डेटा को कभी चुपचाप नहीं हटाता। जिन रो की तारीख़ या रकम पार्स नहीं होती, वे मूल टेक्स्ट के साथ Rejected Rows में जाती हैं, ताकि समस्या दिखे और ठीक हो सके।format="%d/%m/%Y"पक्का करता है कि05/09/20265 सितंबर हो, कभी 9 मई नहीं।- डुप्लिकेट बिज़नेस वाले कॉलम पर हटाए जाते हैं,
SourceFileपर नहीं (दो फ़ाइलों में भेजी गई एक ही बिक्री फिर भी डुप्लिकेट है)। argparseसे कोड बदले बिना कोई भी महीना चला सकते हैं।
6. चलाएँ
python monthly_report.py --input data/2026-09 --output Sales_Report_2026-09.xlsx
आउटपुट:
Rows read: 10 | clean: 7 | rejected: 2
Report saved: C:\...\monthly-report\Sales_Report_2026-09.xlsx
(इस लेसन के सैंपल डेटा के साथ: 10 रो पढ़ी गईं, एक डुप्लिकेट हटा, एक गलत तारीख़ और एक "NA" रकम रिजेक्ट।)
7. इसे आगे बढ़ाएँ (आइडिया)
- बार चार्ट वाली Summary शीट जोड़ें (लेसन 4)।
- पिछले महीने की रिपोर्ट लोड करके महीने-दर-महीने वाला कॉलम जोड़ें।
- Python के
loggingमॉड्यूल से हर रन टेक्स्ट फ़ाइल में लॉग करें। - Scheduling मॉड्यूल में इसे अपने आप चलाएँगे और नतीजा ईमेल करेंगे।
आम गलतियाँ
गलत फ़ोल्डर से चलाना (पूरे पाथ इस्तेमाल करें या प्रोजेक्ट में cd करें)। वर्चुअल एनवायरनमेंट एक्टिवेट करना भूलना। CSV में थोड़े अलग कॉलम नाम ("Amount " स्पेस के साथ — स्क्रिप्ट उन्हें strip करती है, पर "Amt" से टूट जाएगी; ब्रांच अलग हों तो rename स्टेप जोड़ें)।