Mathew K Analytics

Lesson 4 · Pandas Projects

Build a Personal Finance Budget Tracker in Python with Pandas

A complete, standalone tutorial: build six months of synthetic transactions, then categorize, summarize, and track savings with pandas. No prior pandas…

⬇ Download notebookOpen in Colab ↗

What you'll learn

Data

No separate download needed — the notebook creates or downloads everything it uses.

📓 Full notebook

Download .ipynb

Pandas for Personal Finance: Building a Budget Tracker#

  • A complete, standalone tutorial: build six months of synthetic transactions, then categorize, summarize, and track savings with pandas.
  • No prior pandas experience needed. Let's jump straight in.

Before You Start#

  • Open a new Jupyter Notebook in VS Code and select your Python interpreter as the kernel.
  • If pandas isn't installed yet, open a terminal in VS Code and run: pip install pandas

Part 1: Building the Dataset#

import pandas as pd
import numpy as np
print(pd.__version__)
2.3.0

Generating Six Months of Transactions#

rng = np.random.default_rng(seed=44)

expense_categories = {
    'Rent': (1400, 1400),
    'Groceries': (60, 160),
    'Utilities': (80, 220),
    'Dining Out': (15, 90),
    'Entertainment': (10, 70),
    'Transport': (20, 100)
}
print(list(expense_categories.keys()))
['Rent', 'Groceries', 'Utilities', 'Dining Out', 'Entertainment', 'Transport']
rows = []
months = pd.date_range('2026-01-01', periods=6, freq='MS')

for month_start in months:
    rows.append([month_start + pd.Timedelta(days=0), 'Salary Deposit', 'Income', round(float(rng.uniform(3800, 4200)), 2)])
    rows.append([month_start + pd.Timedelta(days=14), 'Salary Deposit', 'Income', round(float(rng.uniform(3800, 4200)), 2)])
    rows.append([month_start + pd.Timedelta(days=1), 'Monthly Rent', 'Rent', -1400.0])
    n_extra = int(rng.integers(10, 18))
    for _ in range(n_extra):
        category = rng.choice([c for c in expense_categories if c != 'Rent'])
        low, high = expense_categories[category]
        day_offset = int(rng.integers(0, 28))
        amount = -round(float(rng.uniform(low, high)), 2)
        rows.append([month_start + pd.Timedelta(days=day_offset), f'{category} purchase', category, amount])

transactions = pd.DataFrame(rows, columns=['date', 'description', 'category', 'amount'])
print(transactions.shape)
(101, 4)
transactions.to_csv('personal_transactions.csv', index=False)
transactions = pd.read_csv('personal_transactions.csv', parse_dates=['date'])
transactions = transactions.sort_values('date').reset_index(drop=True)
transactions.head()
date description category amount
0 2026-01-01 Salary Deposit Income 3849.03
1 2026-01-02 Monthly Rent Rent -1400.00
2 2026-01-03 Dining Out purchase Dining Out -27.17
3 2026-01-03 Transport purchase Transport -74.22
4 2026-01-04 Groceries purchase Groceries -112.39

Part 2: First Look at the Data#

print(transactions.shape)
transactions.info()
(101, 4)
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 101 entries, 0 to 100
Data columns (total 4 columns):
 #   Column       Non-Null Count  Dtype         
---  ------       --------------  -----         
 0   date         101 non-null    datetime64[ns]
 1   description  101 non-null    object        
 2   category     101 non-null    object        
 3   amount       101 non-null    float64       
dtypes: datetime64[ns](1), float64(1), object(2)
memory usage: 3.3+ KB
transactions['category'].value_counts()
category
Dining Out       18
Groceries        18
Transport        18
Entertainment    15
Utilities        14
Income           12
Rent              6
Name: count, dtype: int64

Part 3: Income vs. Expense#

transactions['type'] = np.where(transactions['amount'] > 0, 'Income', 'Expense')
transactions[['description', 'category', 'amount', 'type']].head()
description category amount type
0 Salary Deposit Income 3849.03 Income
1 Monthly Rent Rent -1400.00 Expense
2 Dining Out purchase Dining Out -27.17 Expense
3 Transport purchase Transport -74.22 Expense
4 Groceries purchase Groceries -112.39 Expense
total_income = transactions.loc[transactions['type'] == 'Income', 'amount'].sum()
total_expense = transactions.loc[transactions['type'] == 'Expense', 'amount'].sum()
print(f'Total income: ${total_income:,.2f}')
print(f'Total expenses: ${total_expense:,.2f}')
print(f'Net over 6 months: ${total_income + total_expense:,.2f}')
Total income: $47,868.95
Total expenses: $-15,068.70
Net over 6 months: $32,800.25

Part 4: Monthly Summary#

transactions['month'] = transactions['date'].dt.to_period('M')
monthly_summary = transactions.groupby(['month', 'type'])['amount'].sum().unstack(fill_value=0)
monthly_summary['net'] = monthly_summary['Income'] + monthly_summary['Expense']
monthly_summary.round(2)
type Expense Income net
month
2026-01 -2659.46 7752.28 5092.82
2026-02 -2376.18 7896.60 5520.42
2026-03 -2421.44 7790.93 5369.49
2026-04 -2852.82 8076.63 5223.81
2026-05 -2454.16 8264.16 5810.00
2026-06 -2304.64 8088.35 5783.71

Savings Rate#

monthly_summary['savings_rate_pct'] = (monthly_summary['net'] / monthly_summary['Income'] * 100).round(1)
monthly_summary[['Income', 'Expense', 'net', 'savings_rate_pct']]
type Income Expense net savings_rate_pct
month
2026-01 7752.28 -2659.46 5092.82 65.7
2026-02 7896.60 -2376.18 5520.42 69.9
2026-03 7790.93 -2421.44 5369.49 68.9
2026-04 8076.63 -2852.82 5223.81 64.7
2026-05 8264.16 -2454.16 5810.00 70.3
2026-06 8088.35 -2304.64 5783.71 71.5

Part 5: Category Breakdown#

expenses_only = transactions[transactions['type'] == 'Expense']
category_totals = expenses_only.groupby('category')['amount'].sum().abs().sort_values(ascending=False)
category_totals
category
Rent             8400.00
Utilities        2057.59
Groceries        1900.84
Transport        1091.57
Dining Out        990.34
Entertainment     628.36
Name: amount, dtype: float64
pivot = pd.pivot_table(expenses_only, values='amount', index='month', columns='category', aggfunc='sum', fill_value=0).abs().round(2)
pivot
category Dining Out Entertainment Groceries Rent Transport Utilities
month
2026-01 184.13 122.71 181.11 1400.0 261.78 509.73
2026-02 276.26 57.44 337.25 1400.0 179.66 125.57
2026-03 138.19 102.06 266.00 1400.0 58.14 457.05
2026-04 145.45 111.42 476.66 1400.0 86.63 632.66
2026-05 89.79 69.58 451.14 1400.0 297.14 146.51
2026-06 156.52 165.15 188.68 1400.0 208.22 186.07

Part 6: Running Balance#

starting_balance = 2000
transactions['running_balance'] = starting_balance + transactions['amount'].cumsum()
transactions[['date', 'description', 'amount', 'running_balance']].tail()
date description amount running_balance
96 2026-06-23 Groceries purchase -64.56 35146.99
97 2026-06-24 Transport purchase -74.85 35072.14
98 2026-06-24 Utilities purchase -186.07 34886.07
99 2026-06-27 Dining Out purchase -43.01 34843.06
100 2026-06-28 Entertainment purchase -42.81 34800.25

Part 7: Visualizing the Results#

import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt

fig, ax = plt.subplots(figsize=(8, 5))
colors = ['seagreen' if v >= 0 else 'crimson' for v in monthly_summary['net']]
monthly_summary['net'].plot(kind='bar', ax=ax, color=colors)
ax.set_title('Net Income by Month')
ax.set_ylabel('Net ($)')
ax.axhline(0, color='black', linewidth=0.8)
plt.tight_layout()
plt.savefig('monthly_net.png', dpi=150)
plt.close(fig)
print('Saved monthly_net.png')
Saved monthly_net.png
fig, ax = plt.subplots(figsize=(8, 5))
category_totals.plot(kind='bar', ax=ax, color='darkorange')
ax.set_title('Total Spending by Category (6 Months)')
ax.set_ylabel('Spending ($)')
plt.tight_layout()
plt.savefig('spending_by_category.png', dpi=150)
plt.close(fig)
print('Saved spending_by_category.png')
Saved spending_by_category.png

Wrap-Up: What You Learned#

  • Generating a realistic synthetic six months of income and expenses, then saving and reloading with to_csv and read_csv.
  • Labeling income versus expense with np.where, based on the sign of a column.
  • Monthly grouping with dt.to_period, unstack, and a computed savings rate.
  • Category breakdowns with groupby and a month-by-category pivot_table.
  • Reconstructing a running account balance with cumsum.
  • Color-coded, presentation-ready budget charts with matplotlib.
  • You went from raw synthetic transactions to a full personal budget report with a savings rate and running balance. If you want the next dataset in this series to land in your feed automatically, subscribing is the move see you in the next one.

Found this useful?

All lessons, notebooks and datasets here are free. If they helped you, a coffee keeps new lessons coming.