cmc/cleberg.net

My personal web garden & blog.

clone: git clone https://gitbay.org/cmc/cleberg.net.git

main: content/blog/2022-03-03-financial-database.org · raw

  1#+date:        [2022-03-03 Thu 00:00:00]
  2#+title:       Personal Finance Tracker with SQLite
  3#+description: Building a personal finance tracker with SQLite, Python, and Jupyter.
  4#+slug:        financial-database
  5#+filetags:    :self-hosting:
  6
  7* Personal Financial Tracking
  8
  9For the last 6-ish years, I've tracked my finances in a spreadsheet. This is
 10common practice in the business world, but any good dev will cringe at the
 11thought of storing long-term data in a spreadsheet. A spreadsheet is not for
 12long-term storage or as a source of data to pull data/reports.
 13
 14As I wanted to expand the functionality of my financial data (e.g., adding more
 15reports), I decided to migrate the data into a database. To run reports, I would
 16query the database and use a language like Python or Javascript to process the
 17data, perform calculations, and visualize the data.
 18
 19* SQLite
 20
 21When choosing the type of database I wanted to use for this project, I was split
 22between three options:
 23
 241. MySQL: The database I have the most experience with and have used for years.
 252. PostgreSQL: A database I'm new to, but want to learn.
 263. SQLite: A database that I've used for a couple projects and have moderate
 27   experience.
 28
 29I ended up choosing SQLite since it can be maintained within a single =.sqlite=
 30file, which allows me more flexibility for storage and backup. I keep this file
 31in my cloud storage and pull it up whenever needed.
 32
 33** Visual Editing
 34
 35Since I didn't want to try and import 1000--1500 records into my new database
 36via the command line, I opted to use [[https://sqlitebrowser.org/][DB Browser for SQLite (DB4S)]] as a GUI
 37(graphical user interface) tool. This application is excellent, and I don't see
 38myself going back to the CLI (command line interface) when working in this
 39database.
 40
 41DB4S allows you to copy a range of cells from a spreadsheet and paste it
 42straight into the SQL table. I used this process for all 36 accounts, 1290
 43account statements, and 126 pay statements. Overall, I'm guessing this took
 44anywhere between 4--8 hours. In comparison, it probably took me 2-3 days to
 45initially create the spreadsheet.
 46
 47** Schema
 48
 49The schema for this database is actually extremely simple and involves only
 50three tables (for now):
 51
 521. Accounts
 532. Statements
 543. Payroll
 55
 56*Accounts*
 57
 58The Accounts table contains summary information about an account, such as a car
 59loan or a credit card. By viewing this table, you can find high-level data, such
 60as interest rate, credit line, or owner.
 61
 62#+begin_src sql
 63CREATE TABLE "Accounts" (
 64    "AccountID"     INTEGER NOT NULL UNIQUE,
 65    "AccountType"   TEXT,
 66    "AccountName"   TEXT,
 67    "InterestRate"  NUMERIC,
 68    "CreditLine"    NUMERIC,
 69    "State"         TEXT,
 70    "Owner"         TEXT,
 71    "Co-Owner"      TEXT,
 72    PRIMARY KEY("AccountID" AUTOINCREMENT)
 73)
 74#+end_src
 75
 76*Statements*
 77
 78The Statements table uses the same unique identifier as the Accounts table,
 79meaning you can join the tables to find a monthly statement for any of the
 80accounts listed in the Accounts table. Each statement has an account identified
 81(ID), statement date, and total balance.
 82
 83#+begin_src sql
 84CREATE TABLE "Statements" (
 85    "StatementID"   INTEGER NOT NULL UNIQUE,
 86    "AccountID"     INTEGER,
 87    "StatementDate" INTEGER,
 88    "Balance"       NUMERIC,
 89    PRIMARY KEY("StatementID" AUTOINCREMENT),
 90    FOREIGN KEY("AccountID") REFERENCES "Accounts"("AccountID")
 91)
 92#+end_src
 93
 94*Payroll*
 95
 96The Payroll table is a separate entity, unrelated to the Accounts or Statements
 97tables. This table contains all information you would find on a pay statement
 98from an employer. As you change employers or obtain new perks/benefits, just add
 99new columns to adapt to the new data.
100
101#+begin_src sql
102CREATE TABLE "Payroll" (
103    "PaycheckID"            INTEGER NOT NULL UNIQUE,
104    "PayDate"               TEXT,
105    "Payee"                 TEXT,
106    "Employer"              TEXT,
107    "JobTitle"              TEXT,
108    "IncomeRegular"         NUMERIC,
109    "IncomePTO"             NUMERIC,
110    "IncomeHoliday"         NUMERIC,
111    "IncomeBonus"           NUMERIC,
112    "IncomePTOPayout"       NUMERIC,
113    "IncomeReimbursements"  NUMERIC,
114    "FringeHSA"             NUMERIC,
115    "FringeStudentLoan"     NUMERIC,
116    "Fringe401k"            NUMERIC,
117    "PreTaxMedical"         NUMERIC,
118    "PreTaxDental"          NUMERIC,
119    "PreTaxVision"          NUMERIC,
120    "PreTaxLifeInsurance"   NUMERIC,
121    "PreTax401k"            NUMERIC,
122    "PreTaxParking"         NUMERIC,
123    "PreTaxStudentLoan"     NUMERIC,
124    "PreTaxOther"           NUMERIC,
125    "TaxFederal"            NUMERIC,
126    "TaxSocial"             NUMERIC,
127    "TaxMedicare"           NUMERIC,
128    "TaxState"              NUMERIC,
129    PRIMARY KEY("PaycheckID" AUTOINCREMENT)
130)
131#+end_src
132
133** Python Reporting
134
135Once I created the database tables and imported all my data, the only step left
136was to create a process to report and visualize on various aspects of the data.
137
138In order to explore and create the reports I'm interested in, I utilized a
139two-part process involving Jupyter Notebooks and Python scripts.
140
141*** Step 1: Jupyter Notebooks
142
143When I need to explore data, try different things, and re-run my code
144cell-by-cell, I use Jupyter Notebooks. For example, I explored the =Accounts=
145table until I found the following useful information:
146
147#+begin_src python
148import sqlite3
149import pandas as pd
150import matplotlib
151
152# Set up database filename and connect
153db = "finances.sqlite"
154connection = sqlite3.connect(db)
155df = pd.read_sql_query("SELECT ** FROM Accounts", connection)
156
157# Set global matplotlib variables
158%matplotlib inline
159matplotlib.rcParams['text.color'] = 'white'
160matplotlib.rcParams['axes.labelcolor'] = 'white'
161matplotlib.rcParams['xtick.color'] = 'white'
162matplotlib.rcParams['ytick.color'] = 'white'
163matplotlib.rcParams['legend.labelcolor'] = 'black'
164
165# Display graph
166df.groupby(['AccountType']).sum().plot.pie(title='Credit Line by Account Type', y='CreditLine', figsize=(5,5), autopct='%1.1f%%')
167#+end_src
168
169*** Step 2: Python Scripts
170
171Once I explored enough through the notebooks and had a list of reports I wanted,
172I moved on to create a Python project with the following structure:
173
174#+begin_src txt
175finance/
176├── notebooks/
177│   │   ├── account_summary.ipynb
178│   │   ├── account_details.ipynb
179│   │   └── payroll.ipynb
180├── public/
181│   │   ├── image-01.png
182│   │   └── image-0X.png
183├── src/
184│   └── finance.sqlite
185├── venv/
186├── _init.py
187├── database.py
188├── process.py
189├── requirements.txt
190└── README.md
191#+end_src
192
193This structure allows me to:
194
1951. Compile all required python packages into =requirements.txt= for easy
196   installation if I move to a new machine.
1972. Activate a virtual environment in =venv/= so I don't need to maintain a
198   system-wide Python environment just for this project.
1993. Keep my =notebooks/= folder to continuously explore the data as I see fit.
2004. Maintain a local copy of the database in =src/= for easy access.
2015. Export reports, images, HTML files, etc. to =public/=.
202
203Now, onto the differences between the code in a Jupyter Notebook and the actual
204Python files. To create the report in the Notebook snippet above, I created the
205following function inside =process.py=:
206
207#+begin_src python
208# Create summary pie chart
209def summary_data(accounts: pandas.DataFrame) -> None:
210    accounts_01 = accounts[accounts["Owner"] == "Person01"]
211    accounts_02 = accounts[accounts["Owner"] == "Person02"]
212    for x in range(1, 4):
213        if x == 1:
214            df = accounts
215            account_string = "All Accounts"
216        elif x == 2:
217            df = accounts_01
218            account_string = "Person01's Accounts"
219        elif x == 3:
220            df = accounts_02
221            account_string = "Person02's Accounts"
222        print(f"Generating pie chart summary image for {account_string}...")
223        summary_chart = (
224            df.groupby(["AccountType"])
225            .sum()
226            .plot.pie(
227                title=f"Credit Line by Type for {account_string}",
228                y="CreditLine",
229                autopct="%1.1f%%",
230            )
231        )
232        summary_chart.figure.savefig(f"public/summary_chart_{x}.png", dpi=1200)
233#+end_src
234
235The result? A high-quality pie chart that is read directly by the
236=public/index.html= template I use.
237
238Other charts generated by this project include:
239
240- Charts of account balances over time.
241- Line chart of effective tax rate (taxes divided by taxable income).
242- Salary projections and error limits using past income and inflation rates.
243- Multi-line chart of gross income, taxable income, and net income.
244
245The best thing about this project? I can improve it at any given time, shaping
246it into whatever helps me the most for that time. I imagine that I will be
247introducing an asset tracking table soon to track the depreciating value of
248cars, houses, etc. Who knows what's next?