content/blog/2022-03-03-financial-database.org

0b2895a90c7751ce06e4b26d4db41005ea2f40a9
cleberg.net/content/blog/2022-03-03-financial-database.org rendered · source · history · blame · raw

269 lines · 9337 bytes

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