fx Python for Excel
ENहिन्दी

मिनी प्रोजेक्ट: मासिक रिपोर्ट बनाने वाली स्क्रिप्ट

⏱ 15 min

आप क्या सीखेंगे

  • स्थिति
  • स्क्रिप्ट क्या बनाती है
  • प्रोजेक्ट की बनावट

समझिए

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/2026 5 सितंबर हो, कभी 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 स्टेप जोड़ें)।

अभ्यास

medium3 CSV वाला सैंपल फ़ोल्डर बनाएँ (कम से कम एक गलत तारीख़, एक "NA", एक डुप्लिकेट और कुछ गड़बड़ नाम रखें)। स्क्रिप्ट चलाएँ, रिपोर्ट खोलें और हर शीट जाँचें। फिर "By Product" शीट और "By Region" पर चार्ट जोड़ें।
sample-data पर monthly_report.py चलाएँ। 10 input = 7 clean + 2 rejected + 1 duplicate; South 96000, North 92000, West 61000 और कुल 249000 मिलें। write_report से पहले summaries["By Product"] = good.groupby("Product", as_index=False)["Amount"].sum() जोड़ें। Format के बाद pipeline.py का add_region_chart लगाएँ।

प्रश्नोत्तरी

Rejected Rows शीट क्यों रखें?
ताकि गलत डेटा दिखे और ठीक हो सके, चुपचाप खो न जाए
कौन-सा आर्ग्युमेंट भारतीय तारीख़ों को महीना/दिन पढ़े जाने से रोकता है?
format="%d/%m/%Y"
कोड बदले बिना दूसरे महीने के लिए स्क्रिप्ट कैसे चलाएँ?
कमांड लाइन पर अलग --input/--output दें
मिनी प्रोजेक्ट: मासिक रिपोर्ट बनाने वाली स्क्रिप्ट · हिंदी | ExcelWalaa