The hardware and bandwidth for this mirror is donated by dogado GmbH, the Webhosting and Full Service-Cloud Provider. Check out our Wordpress Tutorial.
If you wish to report a bug, or if you are interested in having us mirror your free-software or open-source project, please feel free to contact us at mirror[@]dogado.de.
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.
register()
Here are some examples of very basic files stored within the package:
library("ledger")
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 Balanc… Assets… 5000 USD ""
## 2 2015-12-31 * <NA> Opening Balanc… Equity… -5000 USD ""
## 3 2016-01-01 * Landlord Rent Assets… -1500 USD ""
## 4 2016-01-01 * Landlord Rent Expens… 1500 USD ""
## 5 2016-01-01 * Brokerage Buy Stock Assets… -1000 USD ""
## 6 2016-01-01 * Brokerage Buy Stock Equity… 1000 USD ""
## 7 2016-01-01 * Brokerage Buy Stock Assets… 4 SP ""
## 8 2016-01-01 * Brokerage Buy Stock Equity… -1000 USD ""
## 9 2016-01-01 * Supermarket Grocery store Expens… 501. USD "Link:…
## 10 2016-01-01 * Supermarket Grocery store Liabil… -501. USD "Link:…
## # ℹ 32 more rows
hledger_file <- system.file("extdata", "example.hledger", package = "ledger")
register(hledger_file)
## # A tibble: 42 × 12
## date mark payee description account amount commodity historical_cost
## <date> <chr> <chr> <chr> <chr> <dbl> <chr> <dbl>
## 1 2015-12-31 * <NA> Opening Ba… Assets… 5000 USD 5000
## 2 2015-12-31 * <NA> Opening Ba… Equity… -5000 USD -5000
## 3 2016-01-01 * Landlo… Rent Assets… -1500 USD -1500
## 4 2016-01-01 * Landlo… Rent Expens… 1500 USD 1500
## 5 2016-01-01 * Broker… Buy Stock Assets… -1000 USD -1000
## 6 2016-01-01 * Broker… Buy Stock Equity… 1000 USD 1000
## 7 2016-01-01 * Broker… Buy Stock Assets… 4 SP 1000
## 8 2016-01-01 * Broker… Buy Stock Equity… -1000 USD -1000
## 9 2016-01-01 * Superm… Grocery st… Expens… 501. USD 501.
## 10 2016-01-01 * Superm… Grocery st… Liabil… -501. USD -501.
## # ℹ 32 more rows
## # ℹ 4 more variables: hc_commodity <chr>, market_value <dbl>,
## # mv_commodity <chr>, id <chr>
beancount_file <- system.file("extdata", "example.beancount", package = "ledger")
register(beancount_file)
## # A tibble: 42 × 13
## date mark payee description account amount commodity historical_cost
## <date> <chr> <chr> <chr> <chr> <dbl> <chr> <dbl>
## 1 2015-12-31 * "" Opening Ba… Assets… 5000 USD 5000
## 2 2015-12-31 * "" Opening Ba… Equity… -5000 USD -5000
## 3 2016-01-01 * "Landl… Rent Assets… -1500 USD -1500
## 4 2016-01-01 * "Landl… Rent Expens… 1500 USD 1500
## 5 2016-01-01 * "Broke… Buy Stock Assets… -1000 USD -1000
## 6 2016-01-01 * "Broke… Buy Stock Equity… 1000 USD 1000
## 7 2016-01-01 * "Broke… Buy Stock Assets… 4 SP 1000
## 8 2016-01-01 * "Broke… Buy Stock Equity… -1000 USD -1000
## 9 2016-01-01 * "Super… Grocery st… Expens… 501. USD 501.
## 10 2016-01-01 * "Super… Grocery st… Liabil… -501. USD -501.
## # ℹ 32 more rows
## # ℹ 5 more variables: hc_commodity <chr>, market_value <dbl>,
## # mv_commodity <chr>, tags <chr>, id <chr>
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)
print(df)
## # A tibble: 2,907 × 13
## date mark payee description account amount commodity historical_cost
## <date> <chr> <chr> <chr> <chr> <dbl> <chr> <dbl>
## 1 2022-01-01 * "" Opening Ba… Assets… 3.78e3 USD 3783.
## 2 2022-01-01 * "" Opening Ba… Equity… -3.78e3 USD -3783.
## 3 2022-01-01 * "" Allowed co… Income… -1.85e4 IRAUSD -18500
## 4 2022-01-01 * "" Allowed co… Assets… 1.85e4 IRAUSD 18500
## 5 2022-01-04 * "BANK… Monthly ba… Assets… -4 e0 USD -4
## 6 2022-01-04 * "BANK… Monthly ba… Expens… 4 e0 USD 4
## 7 2022-01-05 * "Rive… Paying the… Assets… -2.4 e3 USD -2400
## 8 2022-01-05 * "Rive… Paying the… Expens… 2.4 e3 USD 2400
## 9 2022-01-05 * "Jewe… Eating out Liabil… -3.74e1 USD -37.4
## 10 2022-01-05 * "Jewe… Eating out Expens… 3.74e1 USD 37.4
## # ℹ 2,897 more rows
## # ℹ 5 more variables: hc_commodity <chr>, market_value <dbl>,
## # mv_commodity <chr>, tags <chr>, id <chr>
suppressPackageStartupMessages(library("dplyr"))
dplyr::filter(df, grepl("Expenses", account), grepl("trip", tags)) %>%
group_by(trip = tags, account) %>%
summarize(trip_total = sum(amount), .groups = "drop")
## # A tibble: 6 × 3
## trip account trip_total
## <chr> <chr> <dbl>
## 1 trip-los-angeles-2022 Expenses:Food:Alcohol 23.4
## 2 trip-los-angeles-2022 Expenses:Food:Coffee 41.9
## 3 trip-los-angeles-2022 Expenses:Food:Restaurant 613.
## 4 trip-los-angeles-2023 Expenses:Food:Alcohol 14.8
## 5 trip-los-angeles-2023 Expenses:Food:Coffee 12.8
## 6 trip-los-angeles-2023 Expenses:Food:Restaurant 739.
rio::import()
and rio::convert()
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")'
net_worth()
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.
dates <- seq(min(as.Date(df$date)), max(as.Date(df$date)), by="years")
net_worth(bean_example_file, dates)
## # A tibble: 6 × 5
## date commodity net_worth assets liabilities
## <date> <chr> <dbl> <dbl> <dbl>
## 1 2023-01-01 IRAUSD 0 0 0
## 2 2023-01-01 USD 38821. 40256 -1435.
## 3 2023-01-01 VACHR 26 26 0
## 4 2024-01-01 IRAUSD 0 0 0
## 5 2024-01-01 USD 83645. 85451. -1806.
## 6 2024-01-01 VACHR 52 52 0
prune_coa()
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 6500
## 2 Assets USD 99028.
## 3 Assets VACHR 102
## 4 Equity USD -3783.
## 5 Expenses IRAUSD 49000
## 6 Expenses USD 224444.
## 7 Expenses VACHR 208
## 8 Income IRAUSD -55500
## 9 Income USD -313541.
## 10 Income VACHR -310
## 11 Liabilities USD -2382.
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 6500
## 2 Assets:US USD 99028.
## 3 Assets:US VACHR 102
## 4 Equity:Opening-Balances USD -3783.
## 5 Expenses:Financial USD 438.
## 6 Expenses:Food USD 17109.
## 7 Expenses:Health USD 6008.
## 8 Expenses:Home USD 72865.
## 9 Expenses:Taxes IRAUSD 49000
## 10 Expenses:Taxes USD 124784.
## 11 Expenses:Transport USD 3240
## 12 Expenses:Vacation VACHR 208
## 13 Income:US IRAUSD -55500
## 14 Income:US USD -313541.
## 15 Income:US VACHR -310
## 16 Liabilities:AccountsPayable USD 0
## 17 Liabilities:US USD -2382.
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:
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 2024-05-19 IRAUSD 6500 6500 0
## 2 2024-05-19 USD 100688. 102388. -1701.
## 3 2024-05-19 VACHR 110 110 0
print_balance_sheet(prune_coa(df, 2))
## # A tibble: 1 × 3
## account mv_commodity market_value
## <chr> <chr> <dbl>
## 1 Assets:US USD 2273.
## # A tibble: 1 × 3
## account mv_commodity market_value
## <chr> <chr> <dbl>
## 1 Liabilities:US USD -1701.
print_balance_sheet(df)
## # A tibble: 3 × 3
## account mv_commodity market_value
## <chr> <chr> <dbl>
## 1 Assets:US:BofA:Checking USD 1857.
## 2 Assets:US:ETrade:Cash USD 417.
## 3 Assets:US:Vanguard:Cash USD -0.180
## # A tibble: 1 × 3
## account mv_commodity market_value
## <chr> <chr> <dbl>
## 1 Liabilities:US:Chase:Slate USD -1701.
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 `Mar 2024` `Apr 2024` `May 2024`
## <chr> <chr> <dbl> <dbl> <dbl>
## 1 USD expenses 8706. 7496. 2248.
## 2 USD income 11115. 10479. 5240.
## 3 USD net 2410. 2984. 2992.
print_income(prune_coa(df, 2))
## # A tibble: 1 × 5
## account commodity `Mar 2024` `Apr 2024` `May 2024`
## <chr> <chr> <dbl> <dbl> <dbl>
## 1 Income:US USD 11115. 10479. 5240.
print_expenses(prune_coa(df, 2))
## # A tibble: 6 × 5
## account commodity `Mar 2024` `Apr 2024` `May 2024`
## <chr> <chr> <dbl> <dbl> <dbl>
## 1 Expenses:Financial USD 39.8 4 13.0
## 2 Expenses:Food USD 649. 592. 146.
## 3 Expenses:Health USD 194. 194. 96.9
## 4 Expenses:Home USD 2612. 2602. 0
## 5 Expenses:Taxes USD 5092. 3984. 1992.
## 6 Expenses:Transport USD 120 120 0
print_income(df)
## # A tibble: 5 × 5
## account commodity `Mar 2024` `Apr 2024` `May 2024`
## <chr> <chr> <dbl> <dbl> <dbl>
## 1 Income:US:BayBook:GroupTermLife USD 48.6 48.6 24.3
## 2 Income:US:BayBook:Match401k USD 1800 1200 600
## 3 Income:US:BayBook:Salary USD 9231. 9231. 4615.
## 4 Income:US:ETrade:GLD:Dividend USD 95.9 0 0
## 5 Income:US:ETrade:PnL USD -59.8 0 0
print_expenses(df)
## # A tibble: 21 × 5
## account commodity `Mar 2024` `Apr 2024` `May 2024`
## <chr> <chr> <dbl> <dbl> <dbl>
## 1 Expenses:Financial:Commissions USD 35.8 0 8.95
## 2 Expenses:Financial:Fees USD 4 4 4
## 3 Expenses:Food:Groceries USD 282. 270. 66.2
## 4 Expenses:Food:Restaurant USD 367. 321. 79.4
## 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.1 80.0 0
## 11 Expenses:Home:Phone USD 66.4 57.1 0
## 12 Expenses:Home:Rent USD 2400 2400 0
## 13 Expenses:Taxes:Y2023:US:Federal USD 632. 0 0
## 14 Expenses:Taxes:Y2023:US:State USD 476. 0 0
## 15 Expenses:Taxes:Y2024:US:CityNYC USD 350. 350. 175.
## 16 Expenses:Taxes:Y2024:US:Federal USD 2126. 2126. 1063.
## 17 Expenses:Taxes:Y2024:US:Medicare USD 213. 213. 107.
## 18 Expenses:Taxes:Y2024:US:SDI USD 2.24 2.24 1.12
## 19 Expenses:Taxes:Y2024:US:SocSec USD 563. 563. 282.
## 20 Expenses:Taxes:Y2024:US:State USD 730. 730. 365.
## 21 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)
These binaries (installable software) and packages are in development.
They may not be fully stable and should be used with caution. We make no claims about them.
Health stats visible at Monitor.