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:
install.packages("ledger")
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:
install.packages("remotes")
remotes::install_github("trevorld/r-ledger")
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:
ledger
ledger (>= 3.1)
hledger
hledger (>= 1.4)
beancount
beancount (>= 2.0)
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:
pip3 install beancount
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 registers 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 × 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 × 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 × 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,330 × 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 2019-01-01 * "" Opening Balance for checking account Assets:US:BofA:Checking 4119. USD 4119. USD 4119. USD ""
## 2 2019-01-01 * "" Opening Balance for checking account Equity:Opening-Balances -4119. USD -4119. USD -4119. USD ""
## 3 2019-01-01 * "" Allowed contributions for one year Income:US:Federal:PreTax401k -18500 IRAUSD -18500 IRAUSD -18500 IRAUSD ""
## 4 2019-01-01 * "" Allowed contributions for one year Assets:US:Federal:PreTax401k 18500 IRAUSD 18500 IRAUSD 18500 IRAUSD ""
## 5 2019-01-03 * "Hooli" Payroll Assets:US:BofA:Checking 1351. USD 1351. USD 1351. USD ""
## 6 2019-01-03 * "Hooli" Payroll Assets:US:Vanguard:Cash 1200 USD 1200 USD 1200 USD ""
## 7 2019-01-03 * "Hooli" Payroll Income:US:Hooli:Salary -4615. USD -4615. USD -4615. USD ""
## 8 2019-01-03 * "Hooli" Payroll Income:US:Hooli:GroupTermLife -24.3 USD -24.3 USD -24.3 USD ""
## 9 2019-01-03 * "Hooli" Payroll Expenses:Health:Life:GroupTermLife 24.3 USD 24.3 USD 24.3 USD ""
## 10 2019-01-03 * "Hooli" Payroll Expenses:Health:Dental:Insurance 2.9 USD 2.9 USD 2.9 USD ""
## # … with 3,320 more rows
suppressPackageStartupMessages(library("dplyr"))
dplyr::filter(df, grepl("Expenses", account), grepl("trip", tags)) %>%
group_by(trip = tags, account) %>%
summarise(trip_total = sum(amount))
## `summarise()` has grouped output by 'trip'. You can override using the `.groups` argument.
## # A tibble: 7 × 3
## # Groups: trip [3]
## trip account trip_total
## <chr> <chr> <dbl>
## 1 trip-boston-2020 Expenses:Food:Coffee 6.39
## 2 trip-boston-2020 Expenses:Food:Restaurant 234.
## 3 trip-los-angeles-2021 Expenses:Food:Alcohol 52.6
## 4 trip-los-angeles-2021 Expenses:Food:Coffee 24.3
## 5 trip-los-angeles-2021 Expenses:Food:Restaurant 458.
## 6 trip-san-francisco-2019 Expenses:Food:Coffee 30.0
## 7 trip-san-francisco-2019 Expenses:Food:Restaurant 624.
If one has loaded in the ledger package one can also use rio::import to read in the register:
df2 <- rio::import(bean_example_file)
all.equal(df, tibble::as_tibble(df2))
## [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 × 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
net_worth(hledger_file, dates)
## # A tibble: 3 × 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.
net_worth(beancount_file, dates)
## # A tibble: 3 × 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.
net_worth(bean_example_file, dates)
## # A tibble: 0 × 3
## # … with 3 variables: date <date>, commodity <chr>, net_worth <lgl>
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), .groups = "drop")
## # A tibble: 11 × 3
## account mv_commodity market_value
## <chr> <chr> <dbl>
## 1 Assets IRAUSD 0
## 2 Assets USD 119287.
## 3 Assets VACHR 87
## 4 Equity USD -4119.
## 5 Expenses IRAUSD 55500
## 6 Expenses USD 269081.
## 7 Expenses VACHR 288
## 8 Income IRAUSD -55500
## 9 Income USD -377046.
## 10 Income VACHR -375
## 11 Liabilities USD -2248.
df %>% prune_coa(2) %>%
group_by(account, mv_commodity) %>%
summarize(market_value = sum(market_value), .groups = "drop")
## # A tibble: 17 × 3
## account mv_commodity market_value
## <chr> <chr> <dbl>
## 1 Assets:US IRAUSD 0
## 2 Assets:US USD 1.19e+ 5
## 3 Assets:US VACHR 8.7 e+ 1
## 4 Equity:Opening-Balances USD -4.12e+ 3
## 5 Expenses:Financial USD 4.44e+ 2
## 6 Expenses:Food USD 1.87e+ 4
## 7 Expenses:Health USD 7.27e+ 3
## 8 Expenses:Home USD 8.86e+ 4
## 9 Expenses:Taxes IRAUSD 5.55e+ 4
## 10 Expenses:Taxes USD 1.50e+ 5
## 11 Expenses:Transport USD 4.08e+ 3
## 12 Expenses:Vacation VACHR 2.88e+ 2
## 13 Income:US IRAUSD -5.55e+ 4
## 14 Income:US USD -3.77e+ 5
## 15 Income:US VACHR -3.75e+ 2
## 16 Liabilities:AccountsPayable USD 5.68e-14
## 17 Liabilities:US USD -2.25e+ 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), .groups = "drop") %>%
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 × 5
## date commodity net_worth assets liabilities
## <date> <chr> <dbl> <dbl> <dbl>
## 1 2021-11-12 IRAUSD 0 0 0
## 2 2021-11-12 USD 123655. 126076. -2421.
## 3 2021-11-12 VACHR 15 15 0
print_balance_sheet(prune_coa(df, 2))
## # A tibble: 1 × 3
## account mv_commodity market_value
## <chr> <chr> <dbl>
## 1 Assets:US USD 6785.
## # A tibble: 1 × 3
## account mv_commodity market_value
## <chr> <chr> <dbl>
## 1 Liabilities:US USD -2421.
print_balance_sheet(df)
## # A tibble: 3 × 3
## account mv_commodity market_value
## <chr> <chr> <dbl>
## 1 Assets:US:ETrade:Cash USD 6297.
## 2 Assets:US:BofA:Checking USD 489.
## 3 Assets:US:Vanguard:Cash USD -0.0200
## # A tibble: 1 × 3
## account mv_commodity market_value
## <chr> <chr> <dbl>
## 1 Liabilities:US:Chase:Slate USD -2421.
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 × 5
## commodity type `Sep 2021` `Oct 2021` `Nov 2021`
## <chr> <chr> <dbl> <dbl> <dbl>
## 1 USD expenses 7408. 7453. 2227.
## 2 USD income 9437. 9279. 4640.
## 3 USD net 2028. 1826. 2413.
print_income(prune_coa(df, 2))
## # A tibble: 1 × 5
## account commodity `Sep 2021` `Oct 2021` `Nov 2021`
## <chr> <chr> <dbl> <dbl> <dbl>
## 1 Income:US USD 9437. 9279. 4640.
print_expenses(prune_coa(df, 2))
## # A tibble: 6 × 5
## account commodity `Sep 2021` `Oct 2021` `Nov 2021`
## <chr> <chr> <dbl> <dbl> <dbl>
## 1 Expenses:Financial USD 4 39.8 4
## 2 Expenses:Food USD 504. 502. 134.
## 3 Expenses:Health USD 194. 194. 96.9
## 4 Expenses:Home USD 2602. 2614. 0
## 5 Expenses:Taxes USD 3984. 3984. 1992.
## 6 Expenses:Transport USD 120 120 0
print_income(df)
## # A tibble: 3 × 5
## account commodity `Sep 2021` `Oct 2021` `Nov 2021`
## <chr> <chr> <dbl> <dbl> <dbl>
## 1 Income:US:ETrade:GLD:Dividend USD 157. 0 0
## 2 Income:US:Hoogle:GroupTermLife USD 48.6 48.6 24.3
## 3 Income:US:Hoogle:Salary USD 9231. 9231. 4615.
print_expenses(df)
## # A tibble: 19 × 5
## account commodity `Sep 2021` `Oct 2021` `Nov 2021`
## <chr> <chr> <dbl> <dbl> <dbl>
## 1 Expenses:Financial:Commissions USD 0 35.8 0
## 2 Expenses:Financial:Fees USD 4 4 4
## 3 Expenses:Food:Groceries USD 249. 230. 68.5
## 4 Expenses:Food:Restaurant USD 256. 272. 65.2
## 5 Expenses:Health:Dental:Insurance USD 5.8 5.8 2.9
## 6 Expenses:Health:Life:GroupTermLife USD 48.6 48.6 24.3
## 7 Expenses:Health:Medical:Insurance USD 54.8 54.8 27.4
## 8 Expenses:Health:Vision:Insurance USD 84.6 84.6 42.3
## 9 Expenses:Home:Electricity USD 65 65 0
## 10 Expenses:Home:Internet USD 80.0 79.9 0
## 11 Expenses:Home:Phone USD 56.8 68.6 0
## 12 Expenses:Home:Rent USD 2400 2400 0
## 13 Expenses:Taxes:Y2021:US:CityNYC USD 350. 350. 175.
## 14 Expenses:Taxes:Y2021:US:Federal USD 2126. 2126. 1063.
## 15 Expenses:Taxes:Y2021:US:Medicare USD 213. 213. 107.
## 16 Expenses:Taxes:Y2021:US:SDI USD 2.24 2.24 1.12
## 17 Expenses:Taxes:Y2021:US:SocSec USD 563. 563. 282.
## 18 Expenses:Taxes:Y2021:US:State USD 730. 730. 365.
## 19 Expenses:Transport:Tram USD 120 120 0
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)
