ledger
is an R package to import data from plain text accounting software like Ledger, HLedger, and Beancount into an R data frame for convenient analysis, plotting, and export.
Right now it supports reading in the register from ledger
, hledger
, and beancount
files.
To install the last version released to CRAN use the following command in R:
To install the development version of the ledger
package (and its R package dependencies) use the install_github
function from the remotes
package in R:
This package also has some system dependencies that need to be installed depending on which plaintext accounting files you wish to read to be able to read in:
To install hledger run the following in your shell:
stack update && stack install --resolver=lts-14.3 hledger-lib-1.15.2 hledger-1.15.2 hledger-web-1.15 hledger-ui-1.15 --verbosity=error
To install beancount run the following in your shell:
Several pre-compiled Ledger binaries are available (often found in several open source repos).
To run the unit tests you'll also need the suggested R package testthat
.
The main function of this package is register
which reads in the register of a plaintext accounting file. This package also exports S3 methods so one can use rio::import
to read in a register, a net_worth
convenience function, and a prune_coa
convenience function.
Here are some examples of very basic files stored within the package:
library("ledger")
options(width=180)
ledger_file <- system.file("extdata", "example.ledger", package = "ledger")
register(ledger_file)
## # A tibble: 42 x 8
## date mark payee description account amount commodity comment
## <date> <chr> <chr> <chr> <chr> <dbl> <chr> <chr>
## 1 2015-12-31 * <NA> Opening Balances Assets:JT-Checking 5000 USD <NA>
## 2 2015-12-31 * <NA> Opening Balances Equity:Opening -5000 USD <NA>
## 3 2016-01-01 * Landlord Rent Assets:JT-Checking -1500 USD <NA>
## 4 2016-01-01 * Landlord Rent Expenses:Shelter:Rent 1500 USD <NA>
## 5 2016-01-01 * Brokerage Buy Stock Assets:JT-Checking -1000 USD <NA>
## 6 2016-01-01 * Brokerage Buy Stock Equity:Transfer 1000 USD <NA>
## 7 2016-01-01 * Brokerage Buy Stock Assets:JT-Brokerage 4 SP <NA>
## 8 2016-01-01 * Brokerage Buy Stock Equity:Transfer -1000 USD <NA>
## 9 2016-01-01 * Supermarket Grocery store ;; Link: ^grocery Expenses:Food:Grocery 501. USD <NA>
## 10 2016-01-01 * Supermarket Grocery store ;; Link: ^grocery Liabilities:JT-Credit-Card -501. USD <NA>
## # … with 32 more rows
hledger_file <- system.file("extdata", "example.hledger", package = "ledger")
register(hledger_file)
## # A tibble: 42 x 11
## date mark payee description account amount commodity historical_cost hc_commodity market_value mv_commodity
## <date> <chr> <chr> <chr> <chr> <dbl> <chr> <dbl> <chr> <dbl> <chr>
## 1 2015-12-31 * <NA> Opening Balances Assets:JT-Checking 5000 USD 5000 USD 5000 USD
## 2 2015-12-31 * <NA> Opening Balances Equity:Opening -5000 USD -5000 USD -5000 USD
## 3 2016-01-01 * Landlord Rent Assets:JT-Checking -1500 USD -1500 USD -1500 USD
## 4 2016-01-01 * Landlord Rent Expenses:Shelter:Rent 1500 USD 1500 USD 1500 USD
## 5 2016-01-01 * Brokerage Buy Stock Assets:JT-Checking -1000 USD -1000 USD -1000 USD
## 6 2016-01-01 * Brokerage Buy Stock Equity:Transfer 1000 USD 1000 USD 1000 USD
## 7 2016-01-01 * Brokerage Buy Stock Assets:JT-Brokerage 4 SP 1000 USD 2000 USD
## 8 2016-01-01 * Brokerage Buy Stock Equity:Transfer -1000 USD -1000 USD -1000 USD
## 9 2016-01-01 * Supermarket Grocery store Expenses:Food:Grocery 501. USD 501. USD 501. USD
## 10 2016-01-01 * Supermarket Grocery store Liabilities:JT-Credit-Card -501. USD -501. USD -501. USD
## # … with 32 more rows
beancount_file <- system.file("extdata", "example.beancount", package = "ledger")
register(beancount_file)
## # A tibble: 42 x 12
## date mark payee description account amount commodity historical_cost hc_commodity market_value mv_commodity tags
## <chr> <chr> <chr> <chr> <chr> <dbl> <chr> <dbl> <chr> <dbl> <chr> <chr>
## 1 2015-12-31 * "" Opening Balances Assets:JT-Checking 5000 USD 5000 USD 5000 USD ""
## 2 2015-12-31 * "" Opening Balances Equity:Opening -5000 USD -5000 USD -5000 USD ""
## 3 2016-01-01 * Landlord Rent Assets:JT-Checking -1500 USD -1500 USD -1500 USD ""
## 4 2016-01-01 * Landlord Rent Expenses:Shelter:Rent 1500 USD 1500 USD 1500 USD ""
## 5 2016-01-01 * Brokerage Buy Stock Assets:JT-Checking -1000 USD -1000 USD -1000 USD ""
## 6 2016-01-01 * Brokerage Buy Stock Equity:Transfer 1000 USD 1000 USD 1000 USD ""
## 7 2016-01-01 * Brokerage Buy Stock Assets:JT-Brokerage 4 SP 1000 USD 2000 USD ""
## 8 2016-01-01 * Brokerage Buy Stock Equity:Transfer -1000 USD -1000 USD -1000 USD ""
## 9 2016-01-01 * Supermarket Grocery store Expenses:Food:Grocery 501. USD 501. USD 501. USD ""
## 10 2016-01-01 * Supermarket Grocery store Liabilities:JT-Credit-Card -501. USD -501. USD -501. USD ""
## # … with 32 more rows
Here is an example reading in a beancount file generated by bean-example
:
bean_example_file <- tempfile(fileext = ".beancount")
system(paste("bean-example -o", bean_example_file), ignore.stderr=TRUE)
df <- register(bean_example_file)
options(width=240)
print(df)
## # A tibble: 3,206 x 12
## date mark payee description account amount commodity historical_cost hc_commodity market_value mv_commodity tags
## <chr> <chr> <chr> <chr> <chr> <dbl> <chr> <dbl> <chr> <dbl> <chr> <chr>
## 1 2017-01-01 * "" Opening Balance for checking account Assets:US:BofA:Checking 3682. USD 3682. USD 3682. USD ""
## 2 2017-01-01 * "" Opening Balance for checking account Equity:Opening-Balances -3682. USD -3682. USD -3682. USD ""
## 3 2017-01-01 * "" Allowed contributions for one year Income:US:Federal:PreTax401k -18500 IRAUSD -18500 IRAUSD -18500 IRAUSD ""
## 4 2017-01-01 * "" Allowed contributions for one year Assets:US:Federal:PreTax401k 18500 IRAUSD 18500 IRAUSD 18500 IRAUSD ""
## 5 2017-01-03 * RiverBank Properties Paying the rent Assets:US:BofA:Checking -2400 USD -2400 USD -2400 USD ""
## 6 2017-01-03 * RiverBank Properties Paying the rent Expenses:Home:Rent 2400 USD 2400 USD 2400 USD ""
## 7 2017-01-04 * BANK FEES Monthly bank fee Assets:US:BofA:Checking -4 USD -4 USD -4 USD ""
## 8 2017-01-04 * BANK FEES Monthly bank fee Expenses:Financial:Fees 4 USD 4 USD 4 USD ""
## 9 2017-01-05 * Uncle Boons Eating out with Julie Liabilities:US:Chase:Slate -58.9 USD -58.9 USD -58.9 USD ""
## 10 2017-01-05 * Uncle Boons Eating out with Julie Expenses:Food:Restaurant 58.9 USD 58.9 USD 58.9 USD ""
## # … with 3,196 more rows
suppressPackageStartupMessages(library("dplyr"))
dplyr::filter(df, grepl("Expenses", account), grepl("trip", tags)) %>%
group_by(trip = tags, account) %>%
summarise(trip_total = sum(amount))
## # A tibble: 7 x 3
## # Groups: trip [3]
## trip account trip_total
## <chr> <chr> <dbl>
## 1 trip-boston-2019 Expenses:Food:Coffee 29.2
## 2 trip-boston-2019 Expenses:Food:Restaurant 425.
## 3 trip-chicago-2018 Expenses:Food:Alcohol 47.5
## 4 trip-chicago-2018 Expenses:Food:Coffee 28.0
## 5 trip-chicago-2018 Expenses:Food:Restaurant 602
## 6 trip-san-francisco-2017 Expenses:Food:Coffee 35.1
## 7 trip-san-francisco-2017 Expenses:Food:Restaurant 700.
If one has loaded in the ledger
package one can also use rio::import
to read in the register:
## Unrecognized file format. Try specifying with the format argument.
## Unrecognized file format. Try specifying with the format argument.
## [1] TRUE
The main advantage of this is that it allows one to use rio::convert
to easily convert plaintext accounting files to several other file formats such as a csv file. Here is a shell example:
bean-example -o example.beancount
Rscript --default-packages=ledger,rio -e 'convert("example.beancount", "example.csv")'
Some examples of using the net_worth
function using the example files from the register
examples:
dates <- seq(as.Date("2016-01-01"), as.Date("2018-01-01"), by="years")
net_worth(ledger_file, dates)
## # A tibble: 3 x 6
## date commodity net_worth assets liabilities revalued
## <date> <chr> <dbl> <dbl> <dbl> <dbl>
## 1 2016-01-01 USD 5000 5000 0 0
## 2 2017-01-01 USD 4361. 4882 -521. 0
## 3 2018-01-01 USD 6743. 6264 -521. 1000
## # A tibble: 3 x 5
## date commodity net_worth assets liabilities
## <date> <chr> <dbl> <dbl> <dbl>
## 1 2016-01-01 USD 5000 5000 0
## 2 2017-01-01 USD 4361. 4882 -521.
## 3 2018-01-01 USD 6743. 7264 -521.
## # A tibble: 3 x 5
## date commodity net_worth assets liabilities
## <date> <chr> <dbl> <dbl> <dbl>
## 1 2016-01-01 USD 5000 5000 0
## 2 2017-01-01 USD 4361. 4882 -521.
## 3 2018-01-01 USD 6743. 7264 -521.
## # A tibble: 3 x 5
## date commodity net_worth assets liabilities
## <date> <chr> <dbl> <dbl> <dbl>
## 1 2018-01-01 IRAUSD 0 0 0
## 2 2018-01-01 USD 40382. 41529. -1147.
## 3 2018-01-01 VACHR 34 34 0
Some examples using the prune_coa
function to simplify the "Chart of Account" names to a given maximum depth:
suppressPackageStartupMessages(library("dplyr"))
df <- register(bean_example_file) %>% dplyr::filter(!is.na(commodity))
df %>% prune_coa() %>%
group_by(account, mv_commodity) %>%
summarize(market_value = sum(market_value))
## # A tibble: 11 x 3
## # Groups: account [5]
## account mv_commodity market_value
## <chr> <chr> <dbl>
## 1 Assets IRAUSD 0
## 2 Assets USD 109301.
## 3 Assets VACHR -2
## 4 Equity USD -3682.
## 5 Expenses IRAUSD 55500
## 6 Expenses USD 252946.
## 7 Expenses VACHR 352
## 8 Income IRAUSD -55500
## 9 Income USD -353352.
## 10 Income VACHR -350
## 11 Liabilities USD -2926.
df %>% prune_coa(2) %>%
group_by(account, mv_commodity) %>%
summarize(market_value = sum(market_value))
## # A tibble: 17 x 3
## # Groups: account [12]
## account mv_commodity market_value
## <chr> <chr> <dbl>
## 1 Assets:US IRAUSD 0.
## 2 Assets:US USD 1.09e+ 5
## 3 Assets:US VACHR -2.00e+ 0
## 4 Equity:Opening-Balances USD -3.68e+ 3
## 5 Expenses:Financial USD 4.14e+ 2
## 6 Expenses:Food USD 1.78e+ 4
## 7 Expenses:Health USD 6.78e+ 3
## 8 Expenses:Home USD 8.34e+ 4
## 9 Expenses:Taxes IRAUSD 5.55e+ 4
## 10 Expenses:Taxes USD 1.41e+ 5
## 11 Expenses:Transport USD 3.72e+ 3
## 12 Expenses:Vacation VACHR 3.52e+ 2
## 13 Income:US IRAUSD -5.55e+ 4
## 14 Income:US USD -3.53e+ 5
## 15 Income:US VACHR -3.50e+ 2
## 16 Liabilities:AccountsPayable USD 5.68e-14
## 17 Liabilities:US USD -2.93e+ 3
Here is some examples using the functions in the package to help generate various personal accounting reports of the beancount example generated by bean-example
.
First we load the (mainly tidyverse) libraries we'll be using and adjusting terminal output:
options(width=240) # tibble output looks better in wide terminal output
library("ledger")
library("dplyr")
filter <- dplyr::filter
library("ggplot2")
library("scales")
library("tidyr")
library("zoo")
filename <- tempfile(fileext = ".beancount")
system(paste("bean-example -o", filename), ignore.stderr=TRUE)
df <- register(filename) %>% mutate(yearmon = zoo::as.yearmon(date)) %>%
filter(commodity=="USD")
nw <- net_worth(filename)
Then we'll write some convenience functions we'll use over and over again:
print_tibble_rows <- function(df) {
print(df, n=nrow(df))
}
count_beans <- function(df, filter_str = "", ...,
amount = "amount",
commodity="commodity",
cutoff=1e-3) {
commodity <- sym(commodity)
amount_var <- sym(amount)
filter(df, grepl(filter_str, account)) %>%
group_by(account, !!commodity, ...) %>%
summarize(!!amount := sum(!!amount_var)) %>%
filter(abs(!!amount_var) > cutoff & !is.na(!!amount_var)) %>%
arrange(desc(abs(!!amount_var)))
}
Here is some basic balance sheets (using the market value of our assets):
print_balance_sheet <- function(df) {
assets <- count_beans(df, "^Assets",
amount="market_value", commodity="mv_commodity")
print_tibble_rows(assets)
liabilities <- count_beans(df, "^Liabilities",
amount="market_value", commodity="mv_commodity")
print_tibble_rows(liabilities)
}
print(nw)
## # A tibble: 3 x 5
## date commodity net_worth assets liabilities
## <date> <chr> <dbl> <dbl> <dbl>
## 1 2019-09-03 IRAUSD 0 0 0
## 2 2019-09-03 USD 118656. 121202. -2546.
## 3 2019-09-03 VACHR 78 78 0
## # A tibble: 1 x 3
## # Groups: account [1]
## account mv_commodity market_value
## <chr> <chr> <dbl>
## 1 Assets:US USD 3626.
## # A tibble: 1 x 3
## # Groups: account [1]
## account mv_commodity market_value
## <chr> <chr> <dbl>
## 1 Liabilities:US USD -2546.
## # A tibble: 3 x 3
## # Groups: account [3]
## account mv_commodity market_value
## <chr> <chr> <dbl>
## 1 Assets:US:BofA:Checking USD 2946.
## 2 Assets:US:ETrade:Cash USD 680.
## 3 Assets:US:Vanguard:Cash USD 0.11
## # A tibble: 1 x 3
## # Groups: account [1]
## account mv_commodity market_value
## <chr> <chr> <dbl>
## 1 Liabilities:US:Chase:Slate USD -2546.
Here is a basic chart of one's net worth from the beginning of the plaintext accounting file to today by month:
next_month <- function(date) {
zoo::as.Date(zoo::as.yearmon(date) + 1/12)
}
nw_dates <- seq(next_month(min(df$date)), next_month(Sys.Date()), by="months")
df_nw <- net_worth(filename, nw_dates) %>% filter(commodity=="USD")
ggplot(df_nw, aes(x=date, y=net_worth, colour=commodity, group=commodity)) +
geom_line() + scale_y_continuous(labels=scales::dollar)
month_cutoff <- zoo::as.yearmon(Sys.Date()) - 2/12
compute_income <- function(df) {
count_beans(df, "^Income", yearmon) %>%
mutate(income = -amount) %>%
select(-amount) %>% ungroup()
}
print_income <- function(df) {
compute_income(df) %>%
filter(yearmon >= month_cutoff) %>%
spread(yearmon, income, fill=0) %>%
print_tibble_rows()
}
compute_expenses <- function(df) {
count_beans(df, "^Expenses", yearmon) %>%
mutate(expenses = amount) %>%
select(-amount) %>% ungroup()
}
print_expenses <- function(df) {
compute_expenses(df) %>%
filter(yearmon >= month_cutoff) %>%
spread(yearmon, expenses, fill=0) %>%
print_tibble_rows()
}
compute_total <- function(df) {
full_join(compute_income(prune_coa(df)) %>% select(-account),
compute_expenses(prune_coa(df)) %>% select(-account),
by=c("yearmon", "commodity")) %>%
mutate(income = ifelse(is.na(income), 0, income),
expenses = ifelse(is.na(expenses), 0, expenses),
net = income - expenses) %>%
gather(type, amount, -yearmon, -commodity)
}
print_total <- function(df) {
compute_total(df) %>%
filter(yearmon >= month_cutoff) %>%
spread(yearmon, amount, fill=0) %>%
print_tibble_rows()
}
print_total(df)
## # A tibble: 3 x 4
## commodity type `Jul 2019` `Aug 2019`
## <chr> <chr> <dbl> <dbl>
## 1 USD expenses 7421. 9528.
## 2 USD income 10479. 14169.
## 3 USD net 3059. 4641.
## # A tibble: 1 x 4
## account commodity `Jul 2019` `Aug 2019`
## <chr> <chr> <dbl> <dbl>
## 1 Income:US USD 10479. 14169.
## # A tibble: 6 x 4
## account commodity `Jul 2019` `Aug 2019`
## <chr> <chr> <dbl> <dbl>
## 1 Expenses:Financial USD 4 21.9
## 2 Expenses:Food USD 519. 512.
## 3 Expenses:Health USD 194. 291.
## 4 Expenses:Home USD 2600. 2606.
## 5 Expenses:Taxes USD 3984. 5977.
## 6 Expenses:Transport USD 120 120
## # A tibble: 3 x 4
## account commodity `Jul 2019` `Aug 2019`
## <chr> <chr> <dbl> <dbl>
## 1 Income:US:Hooli:GroupTermLife USD 48.6 73.0
## 2 Income:US:Hooli:Match401k USD 1200 250
## 3 Income:US:Hooli:Salary USD 9231. 13846.
## # A tibble: 19 x 4
## account commodity `Jul 2019` `Aug 2019`
## <chr> <chr> <dbl> <dbl>
## 1 Expenses:Financial:Commissions USD 0 17.9
## 2 Expenses:Financial:Fees USD 4 4
## 3 Expenses:Food:Groceries USD 219. 156.
## 4 Expenses:Food:Restaurant USD 301. 357.
## 5 Expenses:Health:Dental:Insurance USD 5.8 8.7
## 6 Expenses:Health:Life:GroupTermLife USD 48.6 73.0
## 7 Expenses:Health:Medical:Insurance USD 54.8 82.1
## 8 Expenses:Health:Vision:Insurance USD 84.6 127.
## 9 Expenses:Home:Electricity USD 65 65
## 10 Expenses:Home:Internet USD 80.0 79.9
## 11 Expenses:Home:Phone USD 54.5 61.3
## 12 Expenses:Home:Rent USD 2400 2400
## 13 Expenses:Taxes:Y2019:US:CityNYC USD 350. 525.
## 14 Expenses:Taxes:Y2019:US:Federal USD 2126. 3189.
## 15 Expenses:Taxes:Y2019:US:Medicare USD 213. 320.
## 16 Expenses:Taxes:Y2019:US:SDI USD 2.24 3.36
## 17 Expenses:Taxes:Y2019:US:SocSec USD 563. 845.
## 18 Expenses:Taxes:Y2019:US:State USD 730. 1095.
## 19 Expenses:Transport:Tram USD 120 120
And here is a plot of income, expenses, and net income over time:
ggplot(compute_total(df), aes(x=yearmon, y=amount, group=commodity, colour=commodity)) +
facet_grid(type ~ .) +
geom_line() + geom_hline(yintercept=0, linetype="dashed") +
scale_x_continuous() + scale_y_continuous(labels=scales::comma)