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
beanquery
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 bean-query (to read beancount files) run the following in your shell:
pip3 install beanquery
Several pre-compiled Ledger binaries are available (often found in several open source repos).
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")
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,674 × 13
## date mark payee description account amount commodity historical_cost
## <date> <chr> <chr> <chr> <chr> <dbl> <chr> <dbl>
## 1 2024-01-01 * "" Opening Ba… Assets… 3.04e3 USD 3041.
## 2 2024-01-01 * "" Opening Ba… Equity… -3.04e3 USD -3041.
## 3 2024-01-01 * "" Allowed co… Income… -1.85e4 IRAUSD -18500
## 4 2024-01-01 * "" Allowed co… Assets… 1.85e4 IRAUSD 18500
## 5 2024-01-03 * "Uncl… Eating out… Liabil… -2.58e1 USD -25.8
## 6 2024-01-03 * "Uncl… Eating out… Expens… 2.58e1 USD 25.8
## 7 2024-01-04 * "BANK… Monthly ba… Assets… -4 e0 USD -4
## 8 2024-01-04 * "BANK… Monthly ba… Expens… 4 e0 USD 4
## 9 2024-01-04 * "Hool… Payroll Assets… 1.35e3 USD 1351.
## 10 2024-01-04 * "Hool… Payroll Assets… 1.2 e3 USD 1200
## # ℹ 2,664 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-boston-2024 Expenses:Food:Restaurant 245.
## 2 trip-los-angeles-2025 Expenses:Food:Alcohol 6.76
## 3 trip-los-angeles-2025 Expenses:Food:Coffee 29.6
## 4 trip-los-angeles-2025 Expenses:Food:Restaurant 443.
## 5 trip-new-york-2025 Expenses:Food:Coffee 52.1
## 6 trip-new-york-2025 Expenses:Food:Restaurant 936.
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.
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 2025-01-01 IRAUSD 0 0 0
## 2 2025-01-01 USD 42788. 43595. -807.
## 3 2025-01-01 VACHR 66 66 0
## 4 2026-01-01 IRAUSD 0 0 0
## 5 2026-01-01 USD 86967. 88872. -1905.
## 6 2026-01-01 VACHR 60 60 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), .groups = "drop")
## # A tibble: 11 × 3
## account mv_commodity market_value
## <chr> <chr> <dbl>
## 1 Assets IRAUSD 10100
## 2 Assets USD 113230.
## 3 Assets VACHR 23
## 4 Equity USD -3041.
## 5 Expenses IRAUSD 45400
## 6 Expenses USD 210495.
## 7 Expenses VACHR 272
## 8 Income IRAUSD -55500
## 9 Income USD -297361.
## 10 Income VACHR -295
## 11 Liabilities USD -2721.
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 1.01e+ 4
## 2 Assets:US USD 1.13e+ 5
## 3 Assets:US VACHR 2.3 e+ 1
## 4 Equity:Opening-Balances USD -3.04e+ 3
## 5 Expenses:Financial USD 3.59e+ 2
## 6 Expenses:Food USD 1.50e+ 4
## 7 Expenses:Health USD 5.72e+ 3
## 8 Expenses:Home USD 6.77e+ 4
## 9 Expenses:Taxes IRAUSD 4.54e+ 4
## 10 Expenses:Taxes USD 1.18e+ 5
## 11 Expenses:Transport USD 3.24e+ 3
## 12 Expenses:Vacation VACHR 2.72e+ 2
## 13 Income:US IRAUSD -5.55e+ 4
## 14 Income:US USD -2.97e+ 5
## 15 Income:US VACHR -2.95e+ 2
## 16 Liabilities:AccountsPayable USD 5.68e-14
## 17 Liabilities:US USD -2.72e+ 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:
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 2026-03-29 IRAUSD 10100 10100 0
## 2 2026-03-29 USD 96917. 99893. -2975.
## 3 2026-03-29 VACHR -41 -41 0
print_balance_sheet(prune_coa(df, 2))
## # A tibble: 1 × 3
## account mv_commodity market_value
## <chr> <chr> <dbl>
## 1 Assets:US USD 4023.
## # A tibble: 1 × 3
## account mv_commodity market_value
## <chr> <chr> <dbl>
## 1 Liabilities:US USD -2975.
print_balance_sheet(df)
## # A tibble: 3 × 3
## account mv_commodity market_value
## <chr> <chr> <dbl>
## 1 Assets:US:BofA:Checking USD 3576.
## 2 Assets:US:ETrade:Cash USD 447.
## 3 Assets:US:Vanguard:Cash USD -0.140
## # A tibble: 1 × 3
## account mv_commodity market_value
## <chr> <chr> <dbl>
## 1 Liabilities:US:Chase:Slate USD -2975.
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 `Jan 2026` `Feb 2026` `Mar 2026`
## <chr> <chr> <dbl> <dbl> <dbl>
## 1 USD expenses 9568. 7352. 5667.
## 2 USD income 15719. 10479. 10587.
## 3 USD net 6151. 3128. 4920.
print_income(prune_coa(df, 2))
## # A tibble: 1 × 5
## account commodity `Jan 2026` `Feb 2026` `Mar 2026`
## <chr> <chr> <dbl> <dbl> <dbl>
## 1 Income:US USD 15719. 10479. 10587.
print_expenses(prune_coa(df, 2))
## # A tibble: 6 × 5
## account commodity `Jan 2026` `Feb 2026` `Mar 2026`
## <chr> <chr> <dbl> <dbl> <dbl>
## 1 Expenses:Financial USD 4 4 13.0
## 2 Expenses:Food USD 577. 449. 551.
## 3 Expenses:Health USD 291. 194. 194.
## 4 Expenses:Home USD 2600. 2600. 0
## 5 Expenses:Taxes USD 5977. 3984. 4789.
## 6 Expenses:Transport USD 120 120 120
print_income(df)
## # A tibble: 4 × 5
## account commodity `Jan 2026` `Feb 2026` `Mar 2026`
## <chr> <chr> <dbl> <dbl> <dbl>
## 1 Income:US:ETrade:GLD:Dividend USD 0 0 108.
## 2 Income:US:Hooli:GroupTermLife USD 73.0 48.6 48.6
## 3 Income:US:Hooli:Match401k USD 1800 1200 1200
## 4 Income:US:Hooli:Salary USD 13846. 9231. 9231.
print_expenses(df)
## # A tibble: 21 × 5
## account commodity `Jan 2026` `Feb 2026` `Mar 2026`
## <chr> <chr> <dbl> <dbl> <dbl>
## 1 Expenses:Financial:Commissions USD 0 0 8.95
## 2 Expenses:Financial:Fees USD 4 4 4
## 3 Expenses:Food:Groceries USD 144. 94.4 242.
## 4 Expenses:Food:Restaurant USD 432. 355. 309.
## 5 Expenses:Health:Dental:Insurance USD 8.7 5.8 5.8
## 6 Expenses:Health:Life:GroupTermLife USD 73.0 48.6 48.6
## 7 Expenses:Health:Medical:Insurance USD 82.1 54.8 54.8
## 8 Expenses:Health:Vision:Insurance USD 127. 84.6 84.6
## 9 Expenses:Home:Electricity USD 65 65 0
## 10 Expenses:Home:Internet USD 79.9 80.0 0
## 11 Expenses:Home:Phone USD 54.8 55.3 0
## 12 Expenses:Home:Rent USD 2400 2400 0
## 13 Expenses:Taxes:Y2025:US:Federal USD 0 0 539.
## 14 Expenses:Taxes:Y2025:US:State USD 0 0 265.
## 15 Expenses:Taxes:Y2026:US:CityNYC USD 525. 350. 350.
## 16 Expenses:Taxes:Y2026:US:Federal USD 3189. 2126. 2126.
## 17 Expenses:Taxes:Y2026:US:Medicare USD 320. 213. 213.
## 18 Expenses:Taxes:Y2026:US:SDI USD 3.36 2.24 2.24
## 19 Expenses:Taxes:Y2026:US:SocSec USD 845. 563. 563.
## 20 Expenses:Taxes:Y2026:US:State USD 1095. 730. 730.
## 21 Expenses:Transport:Tram USD 120 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)

