import streamlit as st import pandas as pd import plotly.express as px from datetime import datetime import os import tempfile import traceback # ------------------------ # Config # ------------------------ st.set_page_config(page_title="Expense Tracker", page_icon="💰", layout="centered") DATA_FILE = os.path.join(os.path.dirname(__file__), "expenses.csv") # ------------------------ # Helpers # ------------------------ def get_empty_df(): return pd.DataFrame(columns=["Date", "Description", "Amount", "Category"]) def load_data(): """Load CSV safely and normalize types. Returns DataFrame.""" if not os.path.exists(DATA_FILE): return get_empty_df() try: df = pd.read_csv(DATA_FILE) # Ensure required columns exist for col in ["Date", "Description", "Amount", "Category"]: if col not in df.columns: df[col] = pd.NA # Parse Date to datetime (coerce errors -> NaT) df["Date"] = pd.to_datetime(df["Date"], errors="coerce") # Coerce Amount to numeric and fill NaNs with 0.0 (won't crash plots) df["Amount"] = pd.to_numeric(df["Amount"], errors="coerce").fillna(0.0) # Ensure Description and Category are strings df["Description"] = df["Description"].astype(str).fillna("") df["Category"] = df["Category"].astype(str).fillna("Other") # Re-order columns df = df[["Date", "Description", "Amount", "Category"]] return df except Exception as e: st.error("Error loading data file. Starting with empty dataset.") st.text(traceback.format_exc()) return get_empty_df() def save_data(df: pd.DataFrame): """Save CSV atomically to avoid partial writes.""" try: df_to_save = df.copy() # Save Date as ISO date (YYYY-MM-DD) for readability df_to_save["Date"] = pd.to_datetime(df_to_save["Date"], errors="coerce").dt.date dirpath = os.path.dirname(DATA_FILE) or "." with tempfile.NamedTemporaryFile("w", delete=False, dir=dirpath, newline='') as tf: df_to_save.to_csv(tf.name, index=False) tf.flush() try: os.fsync(tf.fileno()) except Exception: pass os.replace(tf.name, DATA_FILE) except Exception as e: st.error("Failed to save data.") st.text(traceback.format_exc()) # ------------------------ # Session state for persistent DataFrame between interactions # ------------------------ if "df" not in st.session_state: st.session_state.df = load_data() # Keep a local reference for convenience df = st.session_state.df # ------------------------ # UI - Title # ------------------------ st.title("💰 Personal Expense Tracker") st.markdown("Track your expenses and visualize your spending patterns.") # ------------------------ # Input form # ------------------------ with st.form("expense_form", clear_on_submit=False): st.subheader("Add New Expense") c1, c2 = st.columns(2) with c1: date_input = st.date_input("Date", value=datetime.today().date(), key="date_input") category = st.selectbox( "Category", options=["Food", "Transport", "Entertainment", "Shopping", "Bills", "Healthcare", "Other"], index=0, key="category_input" ) with c2: description = st.text_input("Description", key="description_input") amount = st.number_input("Amount ($)", min_value=0.0, format="%.2f", step=0.5, key="amount_input") submitted = st.form_submit_button("Add Expense") if submitted: # validation if amount <= 0: st.error("Amount must be greater than 0.") elif not description or not description.strip(): st.error("Please enter a description.") else: try: new_row = { "Date": pd.to_datetime(date_input), "Description": description.strip(), "Amount": float(amount), "Category": category or "Other", } # Append to session-state DataFrame st.session_state.df = pd.concat( [st.session_state.df, pd.DataFrame([new_row])], ignore_index=True ) # Persist to disk save_data(st.session_state.df) st.success("Expense added successfully!") # Refresh local reference df = st.session_state.df # Clear form inputs (workaround) st.experimental_rerun() except Exception as e: st.error("Failed to add expense.") st.text(traceback.format_exc()) # ------------------------ # Display data & visualizations # ------------------------ df = st.session_state.df # refresh reference after any changes if df is None or df.empty: st.info("No expenses recorded yet. Add your first expense above!") else: st.subheader("Expense History") # Defensive: ensure Amount is numeric df["Amount"] = pd.to_numeric(df["Amount"], errors="coerce").fillna(0.0) # Summary stats (handle possible empty cases) total_expenses = float(df["Amount"].sum()) avg_expense = float(df["Amount"].mean()) if len(df) > 0 else 0.0 # Largest expense (defensive) largest_amount_display = "$0.00" largest_caption = "" try: if df["Amount"].notna().any() and len(df) > 0: idx = df["Amount"].idxmax() row = df.loc[idx] largest_amount_display = f"${float(row['Amount']):,.2f}" largest_caption = str(row.get("Description", "")) except Exception: pass col1, col2, col3 = st.columns(3) col1.metric("Total Expenses", f"${total_expenses:,.2f}") col2.metric("Average Expense", f"${avg_expense:,.2f}") col3.metric("Largest Expense", largest_amount_display, largest_caption) # Table (most recent first) try: display_df = df.sort_values("Date", ascending=False, na_position="last").reset_index(drop=True) st.dataframe(display_df, hide_index=True, use_container_width=True) except Exception: st.dataframe(df, hide_index=True, use_container_width=True) # Visualizations st.subheader("Spending Analysis") tab1, tab2, tab3 = st.tabs(["By Category", "Over Time", "Detailed Analysis"]) with tab1: try: category_totals = df.groupby("Category", sort=False)["Amount"].sum().reset_index() if category_totals.empty: st.info("No category data to plot yet.") else: fig = px.pie(category_totals, values="Amount", names="Category", title="Expenses by Category") st.plotly_chart(fig, use_container_width=True) except Exception: st.error("Couldn't generate category chart.") st.text(traceback.format_exc()) with tab2: try: # Group by date (daily). Remove rows without a valid date first. df_time = df.dropna(subset=["Date"]).copy() if df_time.empty: st.info("No dated expenses to show over time.") else: df_time = df_time.groupby(pd.Grouper(key="Date", freq="D"))["Amount"].sum().reset_index() fig = px.line(df_time, x="Date", y="Amount", title="Spending Over Time") st.plotly_chart(fig, use_container_width=True) except Exception: st.error("Couldn't generate time series.") st.text(traceback.format_exc()) with tab3: try: category_totals = df.groupby("Category", sort=False)["Amount"].sum().reset_index() if category_totals.empty: st.info("No data for detailed analysis.") else: fig = px.bar(category_totals, x="Category", y="Amount", title="Total Spending by Category") st.plotly_chart(fig, use_container_width=True) except Exception: st.error("Couldn't generate detailed analysis chart.") st.text(traceback.format_exc()) # Download CSV try: csv = df.copy() csv["Date"] = pd.to_datetime(csv["Date"], errors="coerce").dt.date st.download_button( label="Download Expenses as CSV", data=csv.to_csv(index=False), file_name="expenses.csv", mime="text/csv", ) except Exception: st.error("Failed to prepare CSV for download.") st.text(traceback.format_exc()) # ------------------------ # Footer and optional debug # ------------------------ st.markdown("---") st.markdown("Built with Streamlit • Deploy on Hugging Face Spaces") with st.expander("Debug / Data snapshot (expand if you need)"): try: st.write("Data file path:", DATA_FILE) st.write("Rows in memory:", len(st.session_state.df)) st.dataframe(st.session_state.df.head(10)) except Exception: st.text("No debug info available.")