Using python to plot ledger transactions
ledger is a very capable piece of software for double-entry accounting. However, it only provides one out-of-the-box option for visualizing transactions: gnuplot. Rather than learn a non-FOSS tool (gnuplot is not really GNU), I set out to get ledger transactions into pandas. ledger
includes python bindings which worked for 95% of my plotting needs, but some missing journal properties were ultimately a showstopper. Fortunately, the gnuplot
flags (-J and -j) produce output almost ready to be parsed by pandas
.
Using python-ledger
The python bindings are pleasant to use, but require some work to get into pandas
.
# Necessary imports
import matplotlib.pyplot as plt
import ledger
import pandas as pd
# Read ledger journal
journal = ledger.read_journal("/data/ledger/finances.ldg")
Queries are performed to find transactions after the journal
is read.
# Find all Income transactions
posts = journal.query("Income")
However, posts
is not easily digestible by pandas. It is possible to build a parseable list by iterating through each transaction:
income = []
for post in journal.query("Income"):
trans = {'Income':post.amount,'Datetime':post.date}
income.append(trans)
incomeDF = pd.DataFrame(income)
The current incomeDF
has a standard index (1,2,...X) and the Datetime
column.
Datetime | Income | |
---|---|---|
0 | 2016-11-04 | $-115.23 |
1 | 2016-12-01 | $-144.04 |
... | ... | ... |
543 | 2017-04-24 | $-100.00 |
The standard index can be replaced with Datetime
allowing Grouper methods to be used.
incomeDF = incomeDF.set_index(pd.DatetimeIndex(incomeDF['Datetime']))
Income | |
---|---|
Datetime | |
2016-11-04 | $-115.23 |
2016-12-01 | $-144.04 |
... | ... |
2017-04-24 | $-100.00 |
The Income
data is a string and cannot be plotted with pandas. The '$' can be removed and the new string cast as a float.
incomeDf['Income'] = incomeDF['Income'].replace('[\$]','',regex=True).astype(float)
With the transactions in a dataframe anything is possible.
Since the index is Datetime
the transactions can be grouped by month and summed.
monthly = df.groupby(pd.TimeGrouper(freq='M'))
monthly = monthly.sum().ix['2016-06-01':'2016-12-31'].fillna(0).abs()
Creating a dataframe for "Expenses" and combining it with incomeDF
(pd.concat([incomeDF,expensesDF],join='outer')
) allows a plot comparing Income and Expenses to be made.
plt.style.use('fivethirtyeight')
ax = monthly.loc[:,['Income','Expenses']].plot.bar()
ax.set_xticklabels([dt.strftime('%b') for dt in monthly.index.to_pydatetime()])
plt.xlabel('')
plt.title('Income and Expenses Comparison')
rate = 1-(monthly['Expenses'])/monthly['Income']
ax2 = ax.twinx()
ax2.plot(rate.values,'g')
#ax.grid(False)
ax2.grid(False)
ax2.set_ylim([0,1])
ax.legend(loc='center left',bbox_to_anchor=(1.1,0.5))
ax.set_ylabel('Amount ($)')
ax2.set_ylabel('Savings Rate (%)', color='g')
ax2.tick_params('y',colors='g')
Using gnuplot output
The -j
and -J
flags cause ledger to produce gnuplot
friendly output. Fortunately, this output is straightforward: a date column and a transaction column. The -J
provides a running total while -j
produces each individual transaction amount.
...
2017-01-13 144.04
2017-01-13 1438.31
2017-01-13 450
...
Calling the ledger
binary from python is accomplished using the subprocess module.
import subprocess as sp
import io
import pandas as pd
# Run ledger using subprocess
ledger_file = '/data/ledger/finances.ldg'
result = sp.run('ledger -f {} -JMRV reg Assets'.format(ledger_file),shell=True,stdout=sp.PIPE)
output = result.stdout.decode('ascii')
The results from sp.run
are binary encoded and must be decoded to ASCII for python string operations to work.
Pandas' read_csv
expects to read a file. Fortunately, io
provides a way for an in-memory string to be treated as a file with StringIO
.
# 'Fake' csv
f = io.StringIO(output)
# Create Dataframe
df = pd.read_csv(f,delim_whitespace=True,names=['Date','Amount'],parse_dates={'datetime':['Date']},index_col='datetime')
The transactions are trivial to plot now that they are stored in a dataframe.
df.loc['2007-12-01':'2009-10-01'].plot()
Was this information useful or thought provoking? Do you appreciate a webpage free of analytics or ads? Say thanks and help keep this site online by using my Amazon Affilliate URL . I'll receive a small percentage of purchases made within 24 hours of clicking. Or, feel free to donate BTC (1DNwgPQMfoWZqnH78yt6cu4WukJa3h8P1f
) or ETH (0xf3c4a78c24D34E111f272Ac2AC72b1f01ba52DF3
).