Umar4321 commited on
Commit
f10032f
·
verified ·
1 Parent(s): 95192f3

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +242 -0
app.py ADDED
@@ -0,0 +1,242 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import pandas as pd
3
+ import plotly.express as px
4
+ from datetime import datetime
5
+ import os
6
+ import tempfile
7
+ import traceback
8
+
9
+ # ------------------------
10
+ # Config
11
+ # ------------------------
12
+ st.set_page_config(page_title="Expense Tracker", page_icon="💰", layout="centered")
13
+ DATA_FILE = os.path.join(os.path.dirname(__file__), "expenses.csv")
14
+
15
+ # ------------------------
16
+ # Helpers
17
+ # ------------------------
18
+ def get_empty_df():
19
+ return pd.DataFrame(columns=["Date", "Description", "Amount", "Category"])
20
+
21
+ def load_data():
22
+ """Load CSV safely and normalize types. Returns DataFrame."""
23
+ if not os.path.exists(DATA_FILE):
24
+ return get_empty_df()
25
+
26
+ try:
27
+ df = pd.read_csv(DATA_FILE)
28
+ # Ensure required columns exist
29
+ for col in ["Date", "Description", "Amount", "Category"]:
30
+ if col not in df.columns:
31
+ df[col] = pd.NA
32
+
33
+ # Parse Date to datetime (coerce errors -> NaT)
34
+ df["Date"] = pd.to_datetime(df["Date"], errors="coerce")
35
+
36
+ # Coerce Amount to numeric and fill NaNs with 0.0 (won't crash plots)
37
+ df["Amount"] = pd.to_numeric(df["Amount"], errors="coerce").fillna(0.0)
38
+
39
+ # Ensure Description and Category are strings
40
+ df["Description"] = df["Description"].astype(str).fillna("")
41
+ df["Category"] = df["Category"].astype(str).fillna("Other")
42
+
43
+ # Re-order columns
44
+ df = df[["Date", "Description", "Amount", "Category"]]
45
+ return df
46
+ except Exception as e:
47
+ st.error("Error loading data file. Starting with empty dataset.")
48
+ st.text(traceback.format_exc())
49
+ return get_empty_df()
50
+
51
+ def save_data(df: pd.DataFrame):
52
+ """Save CSV atomically to avoid partial writes."""
53
+ try:
54
+ df_to_save = df.copy()
55
+ # Save Date as ISO date (YYYY-MM-DD) for readability
56
+ df_to_save["Date"] = pd.to_datetime(df_to_save["Date"], errors="coerce").dt.date
57
+ dirpath = os.path.dirname(DATA_FILE) or "."
58
+ with tempfile.NamedTemporaryFile("w", delete=False, dir=dirpath, newline='') as tf:
59
+ df_to_save.to_csv(tf.name, index=False)
60
+ tf.flush()
61
+ try:
62
+ os.fsync(tf.fileno())
63
+ except Exception:
64
+ pass
65
+ os.replace(tf.name, DATA_FILE)
66
+ except Exception as e:
67
+ st.error("Failed to save data.")
68
+ st.text(traceback.format_exc())
69
+
70
+ # ------------------------
71
+ # Session state for persistent DataFrame between interactions
72
+ # ------------------------
73
+ if "df" not in st.session_state:
74
+ st.session_state.df = load_data()
75
+
76
+ # Keep a local reference for convenience
77
+ df = st.session_state.df
78
+
79
+ # ------------------------
80
+ # UI - Title
81
+ # ------------------------
82
+ st.title("💰 Personal Expense Tracker")
83
+ st.markdown("Track your expenses and visualize your spending patterns.")
84
+
85
+ # ------------------------
86
+ # Input form
87
+ # ------------------------
88
+ with st.form("expense_form", clear_on_submit=False):
89
+ st.subheader("Add New Expense")
90
+ c1, c2 = st.columns(2)
91
+ with c1:
92
+ date_input = st.date_input("Date", value=datetime.today().date(), key="date_input")
93
+ category = st.selectbox(
94
+ "Category",
95
+ options=["Food", "Transport", "Entertainment", "Shopping", "Bills", "Healthcare", "Other"],
96
+ index=0,
97
+ key="category_input"
98
+ )
99
+ with c2:
100
+ description = st.text_input("Description", key="description_input")
101
+ amount = st.number_input("Amount ($)", min_value=0.0, format="%.2f", step=0.5, key="amount_input")
102
+
103
+ submitted = st.form_submit_button("Add Expense")
104
+ if submitted:
105
+ # validation
106
+ if amount <= 0:
107
+ st.error("Amount must be greater than 0.")
108
+ elif not description or not description.strip():
109
+ st.error("Please enter a description.")
110
+ else:
111
+ try:
112
+ new_row = {
113
+ "Date": pd.to_datetime(date_input),
114
+ "Description": description.strip(),
115
+ "Amount": float(amount),
116
+ "Category": category or "Other",
117
+ }
118
+ # Append to session-state DataFrame
119
+ st.session_state.df = pd.concat(
120
+ [st.session_state.df, pd.DataFrame([new_row])],
121
+ ignore_index=True
122
+ )
123
+ # Persist to disk
124
+ save_data(st.session_state.df)
125
+ st.success("Expense added successfully!")
126
+ # Refresh local reference
127
+ df = st.session_state.df
128
+ # Clear form inputs (workaround)
129
+ st.experimental_rerun()
130
+ except Exception as e:
131
+ st.error("Failed to add expense.")
132
+ st.text(traceback.format_exc())
133
+
134
+ # ------------------------
135
+ # Display data & visualizations
136
+ # ------------------------
137
+ df = st.session_state.df # refresh reference after any changes
138
+
139
+ if df is None or df.empty:
140
+ st.info("No expenses recorded yet. Add your first expense above!")
141
+ else:
142
+ st.subheader("Expense History")
143
+ # Defensive: ensure Amount is numeric
144
+ df["Amount"] = pd.to_numeric(df["Amount"], errors="coerce").fillna(0.0)
145
+
146
+ # Summary stats (handle possible empty cases)
147
+ total_expenses = float(df["Amount"].sum())
148
+ avg_expense = float(df["Amount"].mean()) if len(df) > 0 else 0.0
149
+
150
+ # Largest expense (defensive)
151
+ largest_amount_display = "$0.00"
152
+ largest_caption = ""
153
+ try:
154
+ if df["Amount"].notna().any() and len(df) > 0:
155
+ idx = df["Amount"].idxmax()
156
+ row = df.loc[idx]
157
+ largest_amount_display = f"${float(row['Amount']):,.2f}"
158
+ largest_caption = str(row.get("Description", ""))
159
+ except Exception:
160
+ pass
161
+
162
+ col1, col2, col3 = st.columns(3)
163
+ col1.metric("Total Expenses", f"${total_expenses:,.2f}")
164
+ col2.metric("Average Expense", f"${avg_expense:,.2f}")
165
+ col3.metric("Largest Expense", largest_amount_display, largest_caption)
166
+
167
+ # Table (most recent first)
168
+ try:
169
+ display_df = df.sort_values("Date", ascending=False, na_position="last").reset_index(drop=True)
170
+ st.dataframe(display_df, hide_index=True, use_container_width=True)
171
+ except Exception:
172
+ st.dataframe(df, hide_index=True, use_container_width=True)
173
+
174
+ # Visualizations
175
+ st.subheader("Spending Analysis")
176
+ tab1, tab2, tab3 = st.tabs(["By Category", "Over Time", "Detailed Analysis"])
177
+
178
+ with tab1:
179
+ try:
180
+ category_totals = df.groupby("Category", sort=False)["Amount"].sum().reset_index()
181
+ if category_totals.empty:
182
+ st.info("No category data to plot yet.")
183
+ else:
184
+ fig = px.pie(category_totals, values="Amount", names="Category", title="Expenses by Category")
185
+ st.plotly_chart(fig, use_container_width=True)
186
+ except Exception:
187
+ st.error("Couldn't generate category chart.")
188
+ st.text(traceback.format_exc())
189
+
190
+ with tab2:
191
+ try:
192
+ # Group by date (daily). Remove rows without a valid date first.
193
+ df_time = df.dropna(subset=["Date"]).copy()
194
+ if df_time.empty:
195
+ st.info("No dated expenses to show over time.")
196
+ else:
197
+ df_time = df_time.groupby(pd.Grouper(key="Date", freq="D"))["Amount"].sum().reset_index()
198
+ fig = px.line(df_time, x="Date", y="Amount", title="Spending Over Time")
199
+ st.plotly_chart(fig, use_container_width=True)
200
+ except Exception:
201
+ st.error("Couldn't generate time series.")
202
+ st.text(traceback.format_exc())
203
+
204
+ with tab3:
205
+ try:
206
+ category_totals = df.groupby("Category", sort=False)["Amount"].sum().reset_index()
207
+ if category_totals.empty:
208
+ st.info("No data for detailed analysis.")
209
+ else:
210
+ fig = px.bar(category_totals, x="Category", y="Amount", title="Total Spending by Category")
211
+ st.plotly_chart(fig, use_container_width=True)
212
+ except Exception:
213
+ st.error("Couldn't generate detailed analysis chart.")
214
+ st.text(traceback.format_exc())
215
+
216
+ # Download CSV
217
+ try:
218
+ csv = df.copy()
219
+ csv["Date"] = pd.to_datetime(csv["Date"], errors="coerce").dt.date
220
+ st.download_button(
221
+ label="Download Expenses as CSV",
222
+ data=csv.to_csv(index=False),
223
+ file_name="expenses.csv",
224
+ mime="text/csv",
225
+ )
226
+ except Exception:
227
+ st.error("Failed to prepare CSV for download.")
228
+ st.text(traceback.format_exc())
229
+
230
+ # ------------------------
231
+ # Footer and optional debug
232
+ # ------------------------
233
+ st.markdown("---")
234
+ st.markdown("Built with Streamlit • Deploy on Hugging Face Spaces")
235
+
236
+ with st.expander("Debug / Data snapshot (expand if you need)"):
237
+ try:
238
+ st.write("Data file path:", DATA_FILE)
239
+ st.write("Rows in memory:", len(st.session_state.df))
240
+ st.dataframe(st.session_state.df.head(10))
241
+ except Exception:
242
+ st.text("No debug info available.")