Expense Tracker Project Using Python, CSV, Pandas & Matplotlib with Source Code

Expense Tracker Project Using Python

 

# ============================================================
#  EXPENSE TRACKER
#  CBSE Class 12 Computer Science / Informatics Practices
#  Investigatory Project
#
#  Topics Covered:
#    - File Handling (CSV read/write)
#    - pandas  — DataFrame, filtering, groupby, aggregation
#    - matplotlib — pie chart, bar chart, line chart
#    - datetime module
#    - Functions, Dictionaries, Lists
# ============================================================
#
#  REQUIREMENTS:
#    pip install pandas matplotlib
#
#  HOW TO RUN:
#    python expense_tracker.py
#
#  FILE CREATED:
#    expenses.csv  — stores all expense records
# ============================================================

import pandas as pd
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
from matplotlib.gridspec import GridSpec
import os
import csv
from datetime import datetime, date

# ─────────────────────────────────────────────
#  CONFIGURATION
# ─────────────────────────────────────────────

CSV_FILE   = "expenses.csv"
COLUMNS    = ["ID", "Date", "Category", "Description", "Amount", "Payment_Mode"]

CATEGORIES = [
    "Food & Dining",
    "Transport",
    "Education",
    "Entertainment",
    "Shopping",
    "Health & Medical",
    "Utilities & Bills",
    "Rent",
    "Personal Care",
    "Others",
]

PAYMENT_MODES = ["Cash", "UPI", "Debit Card", "Credit Card", "Net Banking"]

# Colours per category (used in all charts)
CAT_COLORS = {
    "Food & Dining"    : "#e74c3c",
    "Transport"        : "#3498db",
    "Education"        : "#9b59b6",
    "Entertainment"    : "#f39c12",
    "Shopping"         : "#1abc9c",
    "Health & Medical" : "#2ecc71",
    "Utilities & Bills": "#e67e22",
    "Rent"             : "#34495e",
    "Personal Care"    : "#e91e63",
    "Others"           : "#95a5a6",
}

LINE  = "=" * 68
LINE2 = "-" * 68

# ─────────────────────────────────────────────
#  FILE HANDLING
# ─────────────────────────────────────────────

def initialize_file():
    """Create CSV with header if it doesn't exist."""
    if not os.path.exists(CSV_FILE):
        with open(CSV_FILE, "w", newline="") as f:
            writer = csv.writer(f)
            writer.writerow(COLUMNS)
        print(f"  [✓] New file '{CSV_FILE}' created.")

def load_data():
    """Load CSV into a pandas DataFrame."""
    df = pd.read_csv(CSV_FILE)
    if df.empty:
        return df
    df["Date"]   = pd.to_datetime(df["Date"], errors="coerce")
    df["Amount"] = pd.to_numeric(df["Amount"], errors="coerce").fillna(0)
    return df

def save_expense(record: dict):
    """Append one expense record to CSV."""
    with open(CSV_FILE, "a", newline="") as f:
        writer = csv.DictWriter(f, fieldnames=COLUMNS)
        writer.writerow(record)

def next_id():
    """Auto-increment ID."""
    df = load_data()
    if df.empty or "ID" not in df.columns:
        return 1
    return int(df["ID"].max()) + 1

def overwrite_data(df: pd.DataFrame):
    """Write entire DataFrame back to CSV."""
    df.to_csv(CSV_FILE, index=False)

# ─────────────────────────────────────────────
#  HELPERS
# ─────────────────────────────────────────────

def print_header(title):
    print("\n" + LINE)
    print(f"  {title.upper()}")
    print(LINE)

def pause():
    input("\n  Press Enter to continue...")

def pick_from_list(label, options):
    """Show a numbered list and return the chosen item."""
    print(f"\n  {label}:")
    for i, opt in enumerate(options, 1):
        print(f"    {i:>2}. {opt}")
    while True:
        try:
            n = int(input("  Enter number: ").strip())
            if 1 <= n <= len(options):
                return options[n - 1]
            print(f"  [!] Enter 1–{len(options)}")
        except ValueError:
            print("  [!] Enter a valid number.")

def print_table(df: pd.DataFrame):
    """Print a neat table of expense records."""
    if df.empty:
        print("\n  [!] No records to display.\n")
        return
    print()
    print(f"  {'ID':<5} {'Date':<12} {'Category':<20} {'Description':<22} {'Amount':>9} {'Mode'}")
    print("  " + LINE2)
    for _, row in df.iterrows():
        d    = row["Date"].strftime("%d-%m-%Y") if pd.notna(row["Date"]) else "N/A"
        desc = str(row["Description"])[:20]
        print(f"  {int(row['ID']):<5} {d:<12} {str(row['Category']):<20} "
              f"{desc:<22} ₹{row['Amount']:>8,.2f}  {row['Payment_Mode']}")
    print("  " + LINE2)
    print(f"  {'TOTAL':>61} ₹{df['Amount'].sum():>8,.2f}")

# ─────────────────────────────────────────────
#  FEATURE 1 — ADD EXPENSE
# ─────────────────────────────────────────────

def add_expense():
    print_header("Add New Expense")

    # Date
    date_str = input("  Date (YYYY-MM-DD) or press Enter for today: ").strip()
    if not date_str:
        date_str = date.today().isoformat()
    else:
        try:
            datetime.strptime(date_str, "%Y-%m-%d")
        except ValueError:
            print("  [!] Invalid date format. Using today.")
            date_str = date.today().isoformat()

    category = pick_from_list("Select Category", CATEGORIES)
    desc     = input("  Description (brief): ").strip() or "—"

    while True:
        try:
            amount = float(input("  Amount (₹): ").strip())
            if amount > 0:
                break
            print("  [!] Amount must be positive.")
        except ValueError:
            print("  [!] Enter a valid number.")

    mode = pick_from_list("Payment Mode", PAYMENT_MODES)

    record = {
        "ID"           : next_id(),
        "Date"         : date_str,
        "Category"     : category,
        "Description"  : desc,
        "Amount"       : amount,
        "Payment_Mode" : mode,
    }
    save_expense(record)
    print(f"\n  ✓ Expense of ₹{amount:,.2f} added under '{category}'.")
    pause()

# ─────────────────────────────────────────────
#  FEATURE 2 — VIEW ALL EXPENSES
# ─────────────────────────────────────────────

def view_all():
    print_header("All Expenses")
    df = load_data()
    if df.empty:
        print("  [!] No expenses recorded yet.")
        pause(); return
    print(f"  Total Records : {len(df)}")
    print(f"  Total Spent   : ₹{df['Amount'].sum():,.2f}")
    print_table(df.sort_values("Date", ascending=False))
    pause()

# ─────────────────────────────────────────────
#  FEATURE 3 — FILTER EXPENSES
# ─────────────────────────────────────────────

def filter_expenses():
    print_header("Filter Expenses")
    print("  Filter by:")
    print("    1. Category")
    print("    2. Month & Year")
    print("    3. Date Range")
    print("    4. Payment Mode")
    ch = input("  Choice: ").strip()
    df = load_data()

    if ch == "1":
        cat = pick_from_list("Select Category", CATEGORIES)
        result = df[df["Category"] == cat]
        print(f"\n  Expenses under '{cat}':")

    elif ch == "2":
        m = input("  Month (1-12): ").strip()
        y = input("  Year (e.g. 2025): ").strip()
        result = df[(df["Date"].dt.month == int(m)) &
                    (df["Date"].dt.year  == int(y))]
        print(f"\n  Expenses for {m}/{y}:")

    elif ch == "3":
        s = input("  Start date (YYYY-MM-DD): ").strip()
        e = input("  End date   (YYYY-MM-DD): ").strip()
        result = df[(df["Date"] >= s) & (df["Date"] <= e)]
        print(f"\n  Expenses from {s} to {e}:")

    elif ch == "4":
        mode   = pick_from_list("Select Mode", PAYMENT_MODES)
        result = df[df["Payment_Mode"] == mode]
        print(f"\n  Expenses via '{mode}':")

    else:
        print("  [!] Invalid choice."); pause(); return

    print_table(result.sort_values("Date", ascending=False))
    pause()

# ─────────────────────────────────────────────
#  FEATURE 4 — DELETE EXPENSE
# ─────────────────────────────────────────────

def delete_expense():
    print_header("Delete Expense")
    df = load_data()
    if df.empty:
        print("  [!] No records to delete."); pause(); return

    try:
        eid = int(input("  Enter Expense ID to delete: ").strip())
    except ValueError:
        print("  [!] Invalid ID."); pause(); return

    row = df[df["ID"] == eid]
    if row.empty:
        print(f"  [!] No expense with ID {eid}."); pause(); return

    r = row.iloc[0]
    print(f"\n  Found: [{r['Date'].date()}]  {r['Category']}  —  {r['Description']}  ₹{r['Amount']:,.2f}")
    confirm = input("  Delete this record? (yes/no): ").strip().lower()
    if confirm == "yes":
        df = df[df["ID"] != eid]
        overwrite_data(df)
        print("  ✓ Record deleted.")
    else:
        print("  Deletion cancelled.")
    pause()

# ─────────────────────────────────────────────
#  FEATURE 5 — MONTHLY SUMMARY
# ─────────────────────────────────────────────

def monthly_summary():
    print_header("Monthly Summary")
    df = load_data()
    if df.empty:
        print("  [!] No data available."); pause(); return

    df["Month"] = df["Date"].dt.to_period("M")
    summary = df.groupby("Month")["Amount"].agg(
        Total="sum", Count="count", Average="mean"
    ).reset_index()
    summary = summary.sort_values("Month", ascending=False)

    print(f"\n  {'Month':<12} {'Total (₹)':>12} {'Transactions':>14} {'Avg/Txn (₹)':>13}")
    print("  " + "-" * 55)
    for _, row in summary.iterrows():
        print(f"  {str(row['Month']):<12} ₹{row['Total']:>11,.2f} "
              f"{int(row['Count']):>14} ₹{row['Average']:>12,.2f}")
    pause()

# ─────────────────────────────────────────────
#  FEATURE 6 — CATEGORY SUMMARY
# ─────────────────────────────────────────────

def category_summary():
    print_header("Category-Wise Summary")
    df = load_data()
    if df.empty:
        print("  [!] No data available."); pause(); return

    summary = df.groupby("Category")["Amount"].agg(
        Total="sum", Count="count"
    ).reset_index().sort_values("Total", ascending=False)

    total_all = summary["Total"].sum()

    print(f"\n  {'Category':<22} {'Total (₹)':>12} {'Txns':>6} {'Share':>8}")
    print("  " + "-" * 54)
    for _, row in summary.iterrows():
        share = (row["Total"] / total_all * 100) if total_all > 0 else 0
        bar   = "█" * int(share / 3)
        print(f"  {row['Category']:<22} ₹{row['Total']:>11,.2f} "
              f"{int(row['Count']):>6}  {share:>5.1f}% {bar}")
    print("  " + "-" * 54)
    print(f"  {'TOTAL':<22} ₹{total_all:>11,.2f}")
    pause()

# ─────────────────────────────────────────────
#  FEATURE 7 — SET & CHECK BUDGET
# ─────────────────────────────────────────────

BUDGET_FILE = "budget.csv"

def load_budget():
    if not os.path.exists(BUDGET_FILE):
        return {}
    b = {}
    with open(BUDGET_FILE, "r") as f:
        reader = csv.DictReader(f)
        for row in reader:
            b[row["Category"]] = float(row["Budget"])
    return b

def save_budget(budget: dict):
    with open(BUDGET_FILE, "w", newline="") as f:
        writer = csv.DictWriter(f, fieldnames=["Category", "Budget"])
        writer.writeheader()
        for cat, amt in budget.items():
            writer.writerow({"Category": cat, "Budget": amt})

def budget_menu():
    print_header("Budget Manager")
    budget = load_budget()
    df     = load_data()

    print("    1. Set Monthly Budget for a Category")
    print("    2. View Budget vs Actual (Current Month)")
    ch = input("  Choice: ").strip()

    if ch == "1":
        cat = pick_from_list("Select Category", CATEGORIES)
        while True:
            try:
                amt = float(input(f"  Monthly budget for '{cat}' (₹): ").strip())
                if amt >= 0:
                    break
            except ValueError:
                pass
            print("  [!] Enter a valid amount.")
        budget[cat] = amt
        save_budget(budget)
        print(f"  ✓ Budget of ₹{amt:,.2f} set for '{cat}'.")

    elif ch == "2":
        today = date.today()
        month_df = df[(df["Date"].dt.month == today.month) &
                      (df["Date"].dt.year  == today.year)]
        actual = month_df.groupby("Category")["Amount"].sum()

        print(f"\n  Budget vs Actual — {today.strftime('%B %Y')}\n")
        print(f"  {'Category':<22} {'Budget (₹)':>11} {'Actual (₹)':>11} {'Used':>7}  Status")
        print("  " + "-" * 70)

        for cat in CATEGORIES:
            b_amt  = budget.get(cat, 0)
            a_amt  = actual.get(cat, 0)
            used   = (a_amt / b_amt * 100) if b_amt > 0 else 0
            status = "✓ OK" if a_amt <= b_amt else "⚠ OVER"
            if b_amt == 0:
                status = "—"
            print(f"  {cat:<22} ₹{b_amt:>10,.2f} ₹{a_amt:>10,.2f} "
                  f"{used:>6.1f}%  {status}")
    pause()

# ─────────────────────────────────────────────
#  FEATURE 8 — CHARTS (matplotlib)
# ─────────────────────────────────────────────

def charts_menu():
    print_header("Charts & Visualisation")
    print("    1. Pie Chart        — Spending by Category")
    print("    2. Bar Chart        — Monthly Expenses")
    print("    3. Line Chart       — Daily Spending Trend")
    print("    4. Horizontal Bar   — Category Comparison")
    print("    5. Full Dashboard   — All Charts Together")
    ch = input("  Choice: ").strip()

    df = load_data()
    if df.empty:
        print("  [!] No data to visualise. Add some expenses first.")
        pause(); return

    if   ch == "1": plot_pie(df)
    elif ch == "2": plot_monthly_bar(df)
    elif ch == "3": plot_daily_line(df)
    elif ch == "4": plot_category_hbar(df)
    elif ch == "5": plot_dashboard(df)
    else:
        print("  [!] Invalid choice.")
        pause()

# ── Chart 1: Pie ──────────────────────────────
def plot_pie(df):
    cat_totals = df.groupby("Category")["Amount"].sum()
    cat_totals = cat_totals[cat_totals > 0]
    colors     = [CAT_COLORS.get(c, "#cccccc") for c in cat_totals.index]

    fig, ax = plt.subplots(figsize=(8, 6))
    fig.patch.set_facecolor("#1a1a2e")
    ax.set_facecolor("#1a1a2e")

    wedges, texts, autotexts = ax.pie(
        cat_totals.values,
        labels=cat_totals.index,
        autopct="%1.1f%%",
        colors=colors,
        startangle=140,
        pctdistance=0.82,
        wedgeprops={"linewidth": 1.5, "edgecolor": "#1a1a2e"},
    )
    for t in texts:
        t.set_color("white"); t.set_fontsize(9)
    for at in autotexts:
        at.set_color("white"); at.set_fontsize(8); at.set_fontweight("bold")

    ax.set_title("Spending by Category", color="white",
                 fontsize=14, fontweight="bold", pad=16)
    centre = plt.Circle((0, 0), 0.55, color="#1a1a2e")
    ax.add_artist(centre)
    ax.text(0, 0, f"₹{df['Amount'].sum():,.0f}\nTotal",
            ha="center", va="center", color="white",
            fontsize=10, fontweight="bold")

    plt.tight_layout()
    plt.show()

# ── Chart 2: Monthly Bar ─────────────────────
def plot_monthly_bar(df):
    df["Month"] = df["Date"].dt.to_period("M").astype(str)
    monthly     = df.groupby("Month")["Amount"].sum().sort_index()

    fig, ax = plt.subplots(figsize=(10, 5))
    fig.patch.set_facecolor("#0f0c29")
    ax.set_facecolor("#16213e")

    bars = ax.bar(monthly.index, monthly.values,
                  color="#e94560", edgecolor="#0f0c29",
                  linewidth=0.8, zorder=3)

    for bar in bars:
        h = bar.get_height()
        ax.text(bar.get_x() + bar.get_width() / 2, h + 50,
                f"₹{h:,.0f}", ha="center", va="bottom",
                color="white", fontsize=8)

    ax.set_title("Monthly Expenses", color="white",
                 fontsize=14, fontweight="bold")
    ax.set_xlabel("Month", color="#a0a0b0")
    ax.set_ylabel("Total Amount (₹)", color="#a0a0b0")
    ax.tick_params(colors="white", rotation=30)
    ax.yaxis.set_major_formatter(
        plt.FuncFormatter(lambda x, _: f"₹{x:,.0f}"))
    ax.grid(axis="y", color="#2a2a4a", linestyle="--", alpha=0.7, zorder=0)
    for spine in ax.spines.values():
        spine.set_edgecolor("#2a2a4a")

    plt.tight_layout()
    plt.show()

# ── Chart 3: Daily Line ──────────────────────
def plot_daily_line(df):
    daily = df.groupby(df["Date"].dt.date)["Amount"].sum().reset_index()
    daily.columns = ["Date", "Amount"]
    daily = daily.sort_values("Date")
    daily["Cumulative"] = daily["Amount"].cumsum()

    fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(11, 7), sharex=True)
    fig.patch.set_facecolor("#0f0c29")
    for ax in (ax1, ax2):
        ax.set_facecolor("#16213e")

    # Daily spend
    ax1.fill_between(range(len(daily)), daily["Amount"],
                     color="#e94560", alpha=0.4)
    ax1.plot(range(len(daily)), daily["Amount"],
             color="#e94560", linewidth=2, marker="o", markersize=4)
    ax1.set_title("Daily Spending", color="white", fontsize=12, fontweight="bold")
    ax1.set_ylabel("Amount (₹)", color="#a0a0b0")
    ax1.tick_params(colors="white")
    ax1.yaxis.set_major_formatter(
        plt.FuncFormatter(lambda x, _: f"₹{x:,.0f}"))
    ax1.grid(color="#2a2a4a", linestyle="--", alpha=0.5)

    # Cumulative
    ax2.fill_between(range(len(daily)), daily["Cumulative"],
                     color="#6c5ce7", alpha=0.35)
    ax2.plot(range(len(daily)), daily["Cumulative"],
             color="#a29bfe", linewidth=2, marker="s", markersize=4)
    ax2.set_title("Cumulative Spending", color="white",
                  fontsize=12, fontweight="bold")
    ax2.set_ylabel("Amount (₹)", color="#a0a0b0")
    ax2.set_xticks(range(len(daily)))
    ax2.set_xticklabels(
        [str(d) for d in daily["Date"]], rotation=35,
        ha="right", fontsize=7, color="white"
    )
    ax2.yaxis.set_major_formatter(
        plt.FuncFormatter(lambda x, _: f"₹{x:,.0f}"))
    ax2.grid(color="#2a2a4a", linestyle="--", alpha=0.5)
    for spine in ax2.spines.values():
        spine.set_edgecolor("#2a2a4a")

    plt.tight_layout()
    plt.show()

# ── Chart 4: Horizontal Bar ──────────────────
def plot_category_hbar(df):
    cat_totals = df.groupby("Category")["Amount"].sum().sort_values()
    colors     = [CAT_COLORS.get(c, "#cccccc") for c in cat_totals.index]

    fig, ax = plt.subplots(figsize=(10, 6))
    fig.patch.set_facecolor("#0f0c29")
    ax.set_facecolor("#16213e")

    bars = ax.barh(cat_totals.index, cat_totals.values,
                   color=colors, edgecolor="#0f0c29",
                   linewidth=0.8, height=0.6)

    for bar, val in zip(bars, cat_totals.values):
        ax.text(val + cat_totals.max() * 0.01,
                bar.get_y() + bar.get_height() / 2,
                f"₹{val:,.0f}", va="center", color="white", fontsize=9)

    ax.set_title("Category-Wise Comparison", color="white",
                 fontsize=14, fontweight="bold")
    ax.set_xlabel("Total Amount (₹)", color="#a0a0b0")
    ax.tick_params(colors="white")
    ax.xaxis.set_major_formatter(
        plt.FuncFormatter(lambda x, _: f"₹{x:,.0f}"))
    ax.grid(axis="x", color="#2a2a4a", linestyle="--", alpha=0.6)
    for spine in ax.spines.values():
        spine.set_edgecolor("#2a2a4a")

    plt.tight_layout()
    plt.show()

# ── Chart 5: Full Dashboard ──────────────────
def plot_dashboard(df):
    fig = plt.figure(figsize=(14, 9))
    fig.patch.set_facecolor("#0f0c29")
    gs  = GridSpec(2, 3, figure=fig, hspace=0.45, wspace=0.4)

    DARK  = "#16213e"
    WHITE = "white"
    SUB   = "#a0a0b0"

    def style_ax(ax):
        ax.set_facecolor(DARK)
        ax.tick_params(colors=WHITE, labelsize=8)
        for sp in ax.spines.values():
            sp.set_edgecolor("#2a2a4a")
        ax.grid(color="#2a2a4a", linestyle="--", alpha=0.5)

    # Title
    fig.suptitle("💰  EXPENSE TRACKER — DASHBOARD",
                 color=WHITE, fontsize=16, fontweight="bold", y=0.98)

    # ── Top KPI strip ──
    ax_kpi = fig.add_subplot(gs[0, :])
    ax_kpi.set_facecolor("#16213e")
    ax_kpi.axis("off")
    today   = date.today()
    m_df    = df[(df["Date"].dt.month == today.month) &
                 (df["Date"].dt.year  == today.year)]
    kpis = [
        ("Total Spent",       f"₹{df['Amount'].sum():,.2f}"),
        ("This Month",        f"₹{m_df['Amount'].sum():,.2f}"),
        ("Transactions",      str(len(df))),
        ("Avg per Day",       f"₹{df.groupby(df['Date'].dt.date)['Amount'].sum().mean():,.2f}"),
        ("Largest Expense",   f"₹{df['Amount'].max():,.2f}"),
        ("Top Category",      df.groupby('Category')['Amount'].sum().idxmax()),
    ]
    for i, (label, val) in enumerate(kpis):
        x = 0.08 + i * 0.155
        ax_kpi.text(x, 0.75, val,   transform=ax_kpi.transAxes,
                    color="#e94560", fontsize=13, fontweight="bold", ha="center")
        ax_kpi.text(x, 0.2,  label, transform=ax_kpi.transAxes,
                    color=SUB, fontsize=8, ha="center")
    ax_kpi.set_title("Key Metrics", color=WHITE, fontsize=10,
                      loc="left", pad=4)

    # ── Pie ──
    ax1 = fig.add_subplot(gs[1, 0])
    ax1.set_facecolor(DARK)
    cat_totals = df.groupby("Category")["Amount"].sum()
    cat_totals = cat_totals[cat_totals > 0]
    colors     = [CAT_COLORS.get(c,"#ccc") for c in cat_totals.index]
    wedges, _, autotexts = ax1.pie(
        cat_totals.values, autopct="%1.0f%%", colors=colors,
        startangle=140, pctdistance=0.75,
        wedgeprops={"linewidth":1,"edgecolor":DARK}
    )
    for at in autotexts:
        at.set_color(WHITE); at.set_fontsize(7)
    centre = plt.Circle((0,0),0.5,color=DARK)
    ax1.add_artist(centre)
    ax1.set_title("By Category", color=WHITE, fontsize=10)

    # ── Monthly Bar ──
    ax2 = fig.add_subplot(gs[1, 1])
    style_ax(ax2)
    df["Month"] = df["Date"].dt.to_period("M").astype(str)
    monthly = df.groupby("Month")["Amount"].sum().sort_index().tail(6)
    ax2.bar(range(len(monthly)), monthly.values, color="#e94560",
            edgecolor=DARK, zorder=3)
    ax2.set_xticks(range(len(monthly)))
    ax2.set_xticklabels(monthly.index, rotation=30, ha="right",
                        fontsize=7, color=WHITE)
    ax2.set_title("Monthly Trend", color=WHITE, fontsize=10)
    ax2.set_ylabel("₹", color=SUB, fontsize=8)
    ax2.yaxis.set_major_formatter(plt.FuncFormatter(lambda x,_: f"₹{x/1000:.0f}k"))

    # ── Horizontal Bar ──
    ax3 = fig.add_subplot(gs[1, 2])
    style_ax(ax3)
    top5 = df.groupby("Category")["Amount"].sum().nlargest(5).sort_values()
    clrs = [CAT_COLORS.get(c,"#ccc") for c in top5.index]
    ax3.barh(range(len(top5)), top5.values, color=clrs,
             edgecolor=DARK, height=0.5)
    ax3.set_yticks(range(len(top5)))
    ax3.set_yticklabels(top5.index, fontsize=7, color=WHITE)
    ax3.set_title("Top 5 Categories", color=WHITE, fontsize=10)
    ax3.xaxis.set_major_formatter(plt.FuncFormatter(lambda x,_: f"₹{x/1000:.0f}k"))

    plt.savefig("dashboard.png", dpi=120, bbox_inches="tight",
                facecolor=fig.get_facecolor())
    print("  [✓] Dashboard saved as 'dashboard.png'")
    plt.show()

# ─────────────────────────────────────────────
#  FEATURE 9 — STATISTICS REPORT
# ─────────────────────────────────────────────

def statistics_report():
    print_header("Statistics Report")
    df = load_data()
    if df.empty:
        print("  [!] No data available."); pause(); return

    total      = df["Amount"].sum()
    count      = len(df)
    avg_txn    = df["Amount"].mean()
    max_exp    = df.loc[df["Amount"].idxmax()]
    min_exp    = df.loc[df["Amount"].idxmin()]
    top_cat    = df.groupby("Category")["Amount"].sum().idxmax()
    top_mode   = df["Payment_Mode"].value_counts().idxmax()

    daily_avg  = df.groupby(df["Date"].dt.date)["Amount"].sum().mean()
    monthly_df = df.groupby(df["Date"].dt.to_period("M"))["Amount"].sum()

    print(f"\n  {'Total Amount Spent':<30} : ₹{total:>12,.2f}")
    print(f"  {'Total Transactions':<30} : {count:>13}")
    print(f"  {'Average per Transaction':<30} : ₹{avg_txn:>12,.2f}")
    print(f"  {'Average Daily Spending':<30} : ₹{daily_avg:>12,.2f}")
    print(f"  {'Top Spending Category':<30} : {top_cat}")
    print(f"  {'Most Used Payment Mode':<30} : {top_mode}")

    print(f"\n  Highest Single Expense:")
    print(f"    ₹{max_exp['Amount']:,.2f}  |  {max_exp['Category']}  "
          f"|  {max_exp['Description']}  |  {str(max_exp['Date'])[:10]}")

    print(f"\n  Lowest Single Expense:")
    print(f"    ₹{min_exp['Amount']:,.2f}  |  {min_exp['Category']}  "
          f"|  {min_exp['Description']}  |  {str(min_exp['Date'])[:10]}")

    if not monthly_df.empty:
        print(f"\n  Best Month  (lowest) : {monthly_df.idxmin()}  —  ₹{monthly_df.min():,.2f}")
        print(f"  Worst Month (highest): {monthly_df.idxmax()}  —  ₹{monthly_df.max():,.2f}")

    pause()

# ─────────────────────────────────────────────
#  MAIN MENU
# ─────────────────────────────────────────────

def main_menu():
    initialize_file()
    while True:
        print("\n" + LINE)
        print("         💰  EXPENSE TRACKER")
        print("         CBSE Class 12 — pandas + matplotlib Project")
        print(LINE)
        print("   1.  Add New Expense")
        print("   2.  View All Expenses")
        print("   3.  Filter Expenses")
        print("   4.  Delete an Expense")
        print("   5.  Monthly Summary")
        print("   6.  Category-Wise Summary")
        print("   7.  Budget Manager")
        print("   8.  Charts & Visualisation")
        print("   9.  Statistics Report")
        print("   0.  Exit")
        print(LINE)
        ch = input("  Enter choice: ").strip()

        if   ch == "1": add_expense()
        elif ch == "2": view_all()
        elif ch == "3": filter_expenses()
        elif ch == "4": delete_expense()
        elif ch == "5": monthly_summary()
        elif ch == "6": category_summary()
        elif ch == "7": budget_menu()
        elif ch == "8": charts_menu()
        elif ch == "9": statistics_report()
        elif ch == "0":
            print("\n  Thank you for using Expense Tracker. Goodbye!\n")
            break
        else:
            print("  [!] Invalid choice. Enter 0–9.")

# ─────────────────────────────────────────────
#  ENTRY POINT
# ─────────────────────────────────────────────

if __name__ == "__main__":
    main_menu()

 

Copywrite © 2020-2026, CBSE Python,
All Rights Reserved