Fedora Budget Ledger Analysis

Consolidated analysis of Fedora budget ledger (.ldg) files across fiscal years FY18–FY20, covering spend by category, year-over-year trends, and regional allocations.
Published

October 13, 2025

Show the code

import os
import io
import re
import requests
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from pathlib import Path
from datetime import datetime

plt.style.use("seaborn-v0_8")
sns.set_theme(context="notebook", style="whitegrid")
Show the code
# %%
import os
import re
import pandas as pd
from pathlib import Path

# --- Directory with your ledger CSV exports ---
OUTPUT_DIR = Path("/home/jovyan/work/budget/output/")

# --- Gather all CSVs ---
expected_cols = [
    "date",
    "status",
    "payee",
    "account",
    "currency",
    "amount",
    "cleared",
    "notes",
]

csv_files = sorted(OUTPUT_DIR.glob("fedora_fy*.csv"))
print(f"[INFO] Found {len(csv_files)} files: {[f.name for f in csv_files]}")

if not csv_files:
    raise FileNotFoundError(f"No fedora_fy*.csv files found in {OUTPUT_DIR}")

ledger_dfs = []
for f in csv_files:
    fy_match = re.search(r"fy(\d+)", f.name, re.I)
    fy = f"FY{fy_match.group(1)}" if fy_match else "UnknownFY"

    # Load CSV with explicit column names
    df = pd.read_csv(
        f,
        names=expected_cols,
        header=None,
        on_bad_lines="skip",
        dtype=str,
        encoding="utf-8"
    )
    df["fy"] = fy
    ledger_dfs.append(df)

ledger_df = pd.concat(ledger_dfs, ignore_index=True)
print(f"[INFO] Combined ledger shape: {ledger_df.shape}")

# --- Convert and normalize ---
ledger_df["date"] = pd.to_datetime(ledger_df["date"], errors="coerce")
ledger_df["amount_num"] = pd.to_numeric(
    ledger_df["amount"]
    .astype(str)
    .str.replace(",", "")
    .str.replace("$", "")
    .str.replace("€", "")
    .str.strip(),
    errors="coerce",
).fillna(0.0)

ledger_df["category"] = ledger_df["account"].apply(
    lambda x: x.split(":")[0] if isinstance(x, str) else None
)


def extract_expense_type(account):
    if isinstance(account, str) and account.startswith("Expense:"):
        return account.replace("Expense:", "", 1).strip()
    return None

ledger_df["expense_type"] = ledger_df["account"].apply(extract_expense_type)

#ledger_df.loc[ledger_df["category"].str.lower() == "expense", "category"] = None

# Preview
ledger_df[["fy", "account", "category", "expense_type", "amount_num"]].head(10)
[INFO] Found 4 files: ['fedora_fy17.csv', 'fedora_fy18.csv', 'fedora_fy19.csv', 'fedora_fy20.csv']
[INFO] Combined ledger shape: (2012, 9)
fy account category expense_type amount_num
0 FY17 Expense:Transport Expense Transport 601.0
1 FY17 Expense:FinanceCharges:Paypal Expense FinanceCharges:Paypal 24.0
2 FY17 APAC:Q1 APAC None -625.0
3 FY17 Expense:Lodging Expense Lodging 188.0
4 FY17 Expense:FinanceCharges:Paypal Expense FinanceCharges:Paypal 7.5
5 FY17 APAC:Q1 APAC None -195.5
6 FY17 Expense:PrintingReproduction Expense PrintingReproduction 27.0
7 FY17 Expense:FinanceCharges:Paypal Expense FinanceCharges:Paypal 1.0
8 FY17 APAC:Q1 APAC None -28.0
9 FY17 Expense:PrintingReproduction Expense PrintingReproduction 108.0
Show the code
# %%
import re
import pandas as pd
from pathlib import Path

LEDGER_ROOT = Path("/home/jovyan/work/budget/fedora-budget")


def parse_budget_sections(text, fy):
    """
    Parse '~ every ...' budget sections from Fedora ledgers.
    Detects parent allocation, sub-allocations, and computes
    the trailing 'Unallocated' reconciliation automatically.
    """
    entries = []
    lines = text.splitlines()
    current_freq = None
    in_budget = False
    current_parent = None
    current_block = []

    def flush_block():
        """When a budget block ends, compute unallocated and save rows."""
        nonlocal current_block, current_parent
        if not current_block:
            return
        df = pd.DataFrame(current_block)
        alloc_total = df["amount_num"].sum()
        # only reconcile if parent exists and any spend occurred
        if current_parent and alloc_total != 0:
            entries.append({
                "fy": fy,
                "frequency": current_freq,
                "parent_budget": current_parent,
                "account": f"{current_parent}:Unallocated (computed)",
                "amount_raw": None,
                "amount_num": -alloc_total,
                "is_computed_unallocated": True
            })
        entries.extend(current_block)
        current_block = []

    for line in lines:
        line = line.strip()
        if not line:
            continue

        # Start new budget block
        if line.startswith("~"):
            flush_block()
            current_freq = line
            in_budget = True
            current_parent = None
            continue

        # End block when a new dated transaction begins
        if re.match(r"^\d{4}-\d{2}-\d{2}", line):
            flush_block()
            in_budget = False
            current_parent = None
            continue

        if in_budget:
            if line.startswith(";"):
                continue
            # Strip inline comments
            line = re.split(r"\s*;\s*", line)[0].strip()
            # Match account + amount
            m = re.match(r"^([A-Za-z0-9:\-\s&]+?)\s+(-?\$?\d[\d,\.]*)$", line)
            if m:
                account, amount = m.groups()
                amount_str = amount.strip().replace(",", "").replace("$", "")
                try:
                    amount_num = float(amount_str)
                except ValueError:
                    amount_num = 0.0
                # detect parent
                if current_parent is None and amount_num > 0:
                    current_parent = account.split(":")[0]
                current_block.append({
                    "fy": fy,
                    "frequency": current_freq,
                    "parent_budget": current_parent,
                    "account": account.strip(),
                    "amount_raw": amount.strip(),
                    "amount_num": amount_num,
                    "is_computed_unallocated": False
                })
            elif line:
                # final unallocated marker (no $)
                current_block.append({
                    "fy": fy,
                    "frequency": current_freq,
                    "parent_budget": current_parent,
                    "account": line.strip(),
                    "amount_raw": None,
                    "amount_num": 0.0,
                    "is_computed_unallocated": False
                })

    flush_block()  # end of file

    return entries


# --- Parse all FY ledgers ---
rows = []
for fy_dir in LEDGER_ROOT.glob("fy*/ledger"):
    fy = fy_dir.parent.name.upper()
    for ldg in fy_dir.glob("*.ldg"):
        txt = ldg.read_text(encoding="utf-8", errors="ignore")
        parsed = parse_budget_sections(txt, fy)
        if parsed:
            print(f"[INFO] {fy}: parsed {len(parsed)} lines from {ldg.name}")
            rows.extend(parsed)
        else:
            print(f"[WARN] {fy}: no budget sections found in {ldg.name}")

budget_df = pd.DataFrame(rows)
if not budget_df.empty:
    budget_df["category"] = budget_df["account"].apply(lambda x: x.split(":")[0])
    print(f"[INFO] Parsed total {len(budget_df)} rows.")
    display(budget_df.head(10))
else:
    print("[WARN] No budget entries found.")
[INFO] FY18: parsed 57 lines from council.ldg
[INFO] FY18: parsed 15 lines from diversity.ldg
[INFO] FY18: parsed 25 lines from apac.ldg
[INFO] FY18: parsed 38 lines from emea.ldg
[INFO] FY18: parsed 5 lines from summer-coding.ldg
[INFO] FY18: parsed 23 lines from fad.ldg
[INFO] FY18: parsed 190 lines from flock.ldg
[INFO] FY18: parsed 3 lines from mindshare.ldg
[INFO] FY18: parsed 30 lines from na.ldg
[INFO] FY18: parsed 55 lines from latam.ldg
[INFO] FY17: parsed 23 lines from council.ldg
[INFO] FY17: parsed 7 lines from apac.ldg
[INFO] FY17: parsed 63 lines from emea.ldg
[WARN] FY17: no budget sections found in fudcon-latam.ldg
[INFO] FY17: parsed 17 lines from fad.ldg
[WARN] FY17: no budget sections found in flock.ldg
[WARN] FY17: no budget sections found in fudcon-apac.ldg
[INFO] FY17: parsed 7 lines from na.ldg
[INFO] FY17: parsed 5 lines from latam.ldg
[INFO] FY19: parsed 48 lines from council.ldg
[INFO] FY19: parsed 17 lines from diversity.ldg
[INFO] FY19: parsed 20 lines from apac.ldg
[INFO] FY19: parsed 19 lines from emea.ldg
[INFO] FY19: parsed 5 lines from summer-coding.ldg
[INFO] FY19: parsed 7 lines from fad.ldg
[INFO] FY19: parsed 23 lines from flock.ldg
[INFO] FY19: parsed 29 lines from mindshare.ldg
[INFO] FY19: parsed 15 lines from na.ldg
[INFO] FY19: parsed 16 lines from latam.ldg
[INFO] FY20: parsed 15 lines from council.ldg
[INFO] FY20: parsed 3 lines from diversity.ldg
[INFO] FY20: parsed 4 lines from mentored-projects.ldg
[INFO] FY20: parsed 3 lines from fad.ldg
[INFO] FY20: parsed 18 lines from flock.ldg
[INFO] FY20: parsed 39 lines from mindshare.ldg
[INFO] Parsed total 844 rows.
fy frequency parent_budget account amount_raw amount_num is_computed_unallocated category
0 FY18 ~ every 2 years Budget Budget:Unallocated (computed) None -94308.81 True Budget
1 FY18 ~ every 2 years Budget Budget:Red Hat $196000 196000.00 False Budget
2 FY18 ~ every 2 years Budget APAC:Unallocated -$1600 -1600.00 False APAC
3 FY18 ~ every 2 years Budget EMEA:Unallocated -$4100 -4100.00 False EMEA
4 FY18 ~ every 2 years Budget LATAM:Unallocated -$1300 -1300.00 False LATAM
5 FY18 ~ every 2 years Budget NA:Unallocated -$4250 -4250.00 False NA
6 FY18 ~ every 2 years Budget Flock -$85000 -85000.00 False Flock
7 FY18 ~ every 2 years Budget Flock $12,558.81 12558.81 False Flock
8 FY18 ~ every 2 years Budget Mindshare:Unallocated -$9000 -9000.00 False Mindshare
9 FY18 ~ every 2 years Budget Diversity-Inclusion:Unallocated -$9000 -9000.00 False Diversity-Inclusion
Show the code
# %%
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns

plt.style.use("seaborn-v0_8")
sns.set_theme(context="notebook", style="whitegrid")


def plot_fy_waterfall(ledger_df, budget_df, fy):
    """
    For a given fiscal year:
    Start from total Unallocated budget (from .ldg)
    Then step down by spending categories (excluding Expense: lines)
    """
    fy_budget = budget_df[budget_df["fy"].str.upper() == fy.upper()].copy()
    fy_ledger = ledger_df[ledger_df["fy"].str.upper() == fy.upper()].copy()

    # --- 1. total unallocated budget ---
    topline = fy_budget[
        fy_budget["account"].astype(str).str.endswith("Unallocated")
        & (fy_budget["amount_num"] > 0)
    ]["amount_num"].sum()

    # --- 2. actual spending by category ---
    spend_df = (
        fy_ledger[
            ~fy_ledger["account"].astype(str).str.startswith("Expense:")
        ]
        .groupby("category")["amount_num"]
        .sum()
        .reset_index()
    )

    # spending is negative drawdowns; enforce negative direction
    spend_df["amount_num"] = -abs(spend_df["amount_num"])
    spend_df = spend_df.sort_values("amount_num")

    # --- 3. waterfall steps ---
    steps = [topline]
    labels = ["Total Budget"]
    colors = ["#4daf4a"]

    for _, row in spend_df.iterrows():
        steps.append(row["amount_num"])
        labels.append(row["category"])
        colors.append("#e41a1c")

    remaining = topline + spend_df["amount_num"].sum()
    steps.append(remaining)
    labels.append("Remaining")
    colors.append("#999999")

    # compute bases
    base_vals = [0]
    for val in steps[:-1]:
        base_vals.append(base_vals[-1] + val)
    base_vals = base_vals[:-1]

    # --- 4. plot waterfall ---
    fig, ax = plt.subplots(figsize=(10, 6))
    for i, (val, base, label, color) in enumerate(zip(steps, base_vals, labels, colors)):
        ax.bar(i, val, bottom=base, color=color)
        ax.text(i, base + val / 2, f"{val:,.0f}", ha="center", va="center", fontsize=9, color="white")

    ax.axhline(0, color="black", linewidth=1)
    ax.set_xticks(range(len(labels)))
    ax.set_xticklabels(labels, rotation=45, ha="right")
    ax.set_ylabel("Amount ($)")
    ax.set_title(f"FY {fy} — Budget vs Spending Waterfall")
    plt.tight_layout()
    plt.show()

    # --- 5. data table ---
    table_data = []
    running_total = 0
    for label, val in zip(labels, steps):
        running_total += val
        table_data.append({"Step": label, "Amount": val, "Cumulative": running_total})
    display(pd.DataFrame(table_data))


# Example: plot for each FY found
for fy in sorted(budget_df["fy"].unique()):
    plot_fy_waterfall(ledger_df, budget_df, fy)

Step Amount Cumulative
0 Total Budget 93504.19 93504.19
1 Flock -81283.79 12220.40
2 EMEA -24418.44 -12198.04
3 NA -16972.90 -29170.94
4 FADs -14542.81 -43713.75
5 FUDCon LATAM -12197.90 -55911.65
6 FUDCon APAC -11669.91 -67581.56
7 D&I -6500.00 -74081.56
8 APAC -6434.13 -80515.69
9 LATAM -6239.75 -86755.44
10 Remaining -86755.44 -173510.88

Step Amount Cumulative
0 Total Budget 141017.08 141017.08
1 External -217508.58 -76491.50
2 Budget -217500.27 -293991.77
3 Flock -93432.90 -387424.67
4 FADs -35615.47 -423040.14
5 EMEA -16084.10 -439124.24
6 Diversity-Inclusion -15138.06 -454262.30
7 NA -10082.09 -464344.39
8 Mindshare -8662.94 -473007.33
9 LATAM -5866.05 -478873.38
10 Summer Coding -3254.30 -482127.68
11 Council -1393.00 -483520.68
12 APAC -1358.19 -484878.87
13 Remaining -484878.87 -969757.74

Step Amount Cumulative
0 Total Budget 73049.80 73049.80
1 Flock -99726.42 -26676.62
2 Council -61801.47 -88478.09
3 Diversity-Inclusion -20075.22 -108553.31
4 FADs -14318.66 -122871.97
5 Mindshare -10982.36 -133854.33
6 NA -6400.52 -140254.85
7 LATAM -6372.69 -146627.54
8 EMEA -3081.56 -149709.10
9 Summer Coding -2234.84 -151943.94
10 APAC -1514.75 -153458.69
11 Remaining -153458.69 -306917.38

Step Amount Cumulative
0 Total Budget 50300.00 50300.00
1 Flock -94721.65 -44421.65
2 Expenses -53215.78 -97637.43
3 Mindshare -8479.32 -106116.75
4 Council -4274.95 -110391.70
5 Mentored Projects -3022.93 -113414.63
6 Diversity-Inclusion -198.19 -113612.82
7 Remaining -113612.82 -227225.64
Show the code
# %%
summary = (
    ledger_df.groupby(["fy", "category", "account"])["amount_num"].sum().reset_index()
)
summary_pivot = summary.pivot_table(
    index=["category"], columns="fy", values="amount_num", aggfunc="sum"
).fillna(0)
summary_pivot.round(2)
fy FY17 FY18 FY19 FY20
category
APAC -6434.13 -1358.19 -1514.75 0.00
Budget 0.00 217500.27 0.00 0.00
Council 0.00 -1393.00 -61801.47 -4274.95
D&I -6500.00 0.00 0.00 0.00
Diversity-Inclusion 0.00 -15138.06 -20075.22 -198.19
EMEA -24418.44 -16084.10 -3081.56 0.00
Expense 180259.63 190895.41 226508.49 57481.26
Expenses 0.00 0.00 0.00 53215.78
External 0.00 -217508.58 0.00 0.00
FADs -14542.81 -35615.47 -14318.66 0.00
FUDCon APAC -11669.91 0.00 0.00 0.00
FUDCon LATAM -12197.90 0.00 0.00 0.00
Flock -81283.79 -93432.90 -99726.42 -94721.65
LATAM -6239.75 -5866.05 -6372.69 0.00
Mentored Projects 0.00 0.00 0.00 -3022.93
Mindshare 0.00 -8662.94 -10982.36 -8479.32
NA -16972.90 -10082.09 -6400.52 0.00
Summer Coding 0.00 -3254.30 -2234.84 0.00
Show the code
pd.set_option("display.max_rows", None)
pd.set_option("display.max_columns", None)
pd.set_option("display.width", 0)
pd.set_option("display.colheader_justify", "center")

summary = (
    ledger_df.groupby(["fy", "category", "account"])["amount_num"].sum().reset_index()
)
summary_pivot = summary.pivot_table(
    index=["category", "account"], columns="fy", values="amount_num", aggfunc="sum"
).fillna(0)
summary_pivot.round(2)
fy FY17 FY18 FY19 FY20
category account
APAC APAC:FY17 Expenses 0.00 -582.69 0.00 0.00
APAC:FY18 Expenses 0.00 0.00 -112.45 0.00
APAC:Q1 -1223.00 0.00 0.00 0.00
APAC:Q1:DFD Singapore 0.00 0.00 -53.45 0.00
APAC:Q1:OSCAL 0.00 0.00 -500.00 0.00
APAC:Q1:Release Party BLR 0.00 0.00 -61.04 0.00
APAC:Q1:Release Party Beijing 0.00 0.00 -85.37 0.00
APAC:Q2 -3906.38 0.00 0.00 0.00
APAC:Q2:Devconf.in 0.00 0.00 -647.10 0.00
APAC:Q2:F26 RP 0.00 -80.25 0.00 0.00
APAC:Q2:Pune Meetup 0.00 0.00 -55.34 0.00
APAC:Q2:Swag 0.00 -508.55 0.00 0.00
APAC:Q3 -786.34 0.00 0.00 0.00
APAC:Q4 -518.41 0.00 0.00 0.00
APAC:Q4:F27 RP 0.00 -89.09 0.00 0.00
APAC:Q4:Trans-sprint 0.00 -97.61 0.00 0.00
Budget Budget:Flock:Copays 0.00 5395.80 0.00 0.00
Budget:Flock:Other 0.00 27.00 0.00 0.00
Budget:Flock:Registration 0.00 3858.60 0.00 0.00
Budget:Flock:Sponsors 0.00 5000.00 0.00 0.00
Budget:GSoC 0.00 5700.00 0.00 0.00
Budget:LATAM DevConf Panama Refund 0.00 1433.70 0.00 0.00
Budget:Other 0.00 85.17 0.00 0.00
Budget:Red Hat 0.00 196000.00 0.00 0.00
Council Council:Council Hack 0.00 0.00 -16925.13 0.00
Council:Discourse 0.00 0.00 -7208.38 0.00
Council:Flock 0.00 0.00 -500.00 0.00
Council:Flock 2019 0.00 0.00 -34068.00 0.00
Council:Hack:Council 0.00 0.00 0.00 -3299.32
Council:Hack:Docs Internationalization 0.00 0.00 0.00 -975.63
Council:Other:CI 0.00 0.00 -1698.82 0.00
Council:Other:Fedora Loves .NET 0.00 -642.84 -224.15 0.00
Council:Other:Fedora Loves Python 0.00 0.00 -579.85 0.00
Council:Other:GrimoireCon 0.00 -126.80 0.00 0.00
Council:Other:Microphone 0.00 -99.06 0.00 0.00
Council:Other:Podcast 0.00 0.00 -597.14 0.00
Council:Other:Translation Sprint T-shirts 0.00 -524.30 0.00 0.00
D&I D&I -6500.00 0.00 0.00 0.00
Diversity-Inclusion Diversity-Inclusion:FWD 0.00 0.00 0.00 -198.19
Diversity-Inclusion:FWD:Bangalore 0.00 -94.39 0.00 0.00
Diversity-Inclusion:FWD:Brno 0.00 -99.65 0.00 0.00
Diversity-Inclusion:FWD:Islampur 0.00 0.00 -98.87 0.00
Diversity-Inclusion:FWD:Lima 0.00 -72.77 -57.25 0.00
Diversity-Inclusion:FWD:Managua 0.00 -55.49 0.00 0.00
Diversity-Inclusion:FWD:Mexico 0.00 0.00 -47.03 0.00
Diversity-Inclusion:FWD:Munich 0.00 0.00 -121.82 0.00
Diversity-Inclusion:FWD:Outreachy 0.00 0.00 -19500.00 0.00
Diversity-Inclusion:FWD:Prishtina 0.00 -18.35 0.00 0.00
Diversity-Inclusion:FWD:Pune 0.00 -19.99 0.00 0.00
Diversity-Inclusion:FWD:Rochester 0.00 0.00 -64.10 0.00
Diversity-Inclusion:FWD:Stickers 0.00 -714.98 0.00 0.00
Diversity-Inclusion:FWD:Tirana 0.00 -62.44 0.00 0.00
Diversity-Inclusion:FWD:Trieste 0.00 0.00 -70.58 0.00
Diversity-Inclusion:Outreachy 0.00 -13000.00 0.00 0.00
Diversity-Inclusion:RGSoC 0.00 -1000.00 0.00 0.00
Diversity-Inclusion:Swag 0.00 0.00 -115.57 0.00
EMEA EMEA:Previous Year -5365.79 0.00 0.00 0.00
EMEA:Q1:CLT 2017 0.00 -342.84 0.00 0.00
EMEA:Q1:CLT 2018 0.00 0.00 -303.24 0.00
EMEA:Q1:Chemnitzer Linuxtage 2016 -270.29 0.00 0.00 0.00
EMEA:Q1:DORS-CLUC 0.00 0.00 -273.80 0.00
EMEA:Q1:DORS-CLUC 2016 -1372.95 0.00 0.00 0.00
EMEA:Q1:DORS/CLUC 2017 0.00 -314.36 0.00 0.00
EMEA:Q1:F27 Parties 0.00 0.00 -156.59 0.00
EMEA:Q1:F28 Parties 0.00 0.00 -71.69 0.00
EMEA:Q1:FSAD-Milan 2017 0.00 -173.38 0.00 0.00
EMEA:Q1:Fosscom 2016 -197.53 0.00 0.00 0.00
EMEA:Q1:Linux Weekend Tirana 2017 0.00 -138.66 0.00 0.00
EMEA:Q1:Mozilla & Fedora Activity Day -69.96 0.00 0.00 0.00
EMEA:Q1:OSCAL 0.00 0.00 -1791.45 0.00
EMEA:Q1:OSCAL 2016 -878.23 0.00 0.00 0.00
EMEA:Q1:OSCAL 2017 0.00 -1212.28 0.00 0.00
EMEA:Q1:Pending Tickets From FY17 0.00 -290.53 0.00 0.00
EMEA:Q1:PyCon SK -916.50 0.00 0.00 0.00
EMEA:Q1:PyCon SK 2017 0.00 -464.57 0.00 0.00
EMEA:Q1:Swag 0.00 0.00 -115.57 0.00
EMEA:Q1:Swag Production 0.00 -277.96 0.00 0.00
EMEA:Q1:nullcon 2018 0.00 0.00 -55.72 0.00
EMEA:Q2:EuroPython 2016 -1410.33 0.00 0.00 0.00
EMEA:Q2:F26 Parties 0.00 -278.07 0.00 0.00
EMEA:Q2:Fedora Localization Sprint - Albania -29.63 0.00 0.00 0.00
EMEA:Q2:Fedora Women Day -72.82 0.00 0.00 0.00
EMEA:Q2:Fedorators 0.00 -729.13 0.00 0.00
EMEA:Q2:Flock -1123.66 0.00 0.00 0.00
EMEA:Q2:FrOSCon 2016 -160.85 0.00 0.00 0.00
EMEA:Q2:FrOSCon 2017 0.00 -765.59 0.00 0.00
EMEA:Q2:FrOSCon 2018 0.00 0.00 -313.50 0.00
EMEA:Q2:Release Party F24 -196.14 0.00 0.00 0.00
EMEA:Q2:Swag Production 0.00 -471.17 0.00 0.00
EMEA:Q3:34C3 0.00 -338.21 0.00 0.00
EMEA:Q3:BalCon 2k16 -526.00 0.00 0.00 0.00
EMEA:Q3:FOSSCOMM 2017 0.00 -205.14 0.00 0.00
EMEA:Q3:FSFE European Summit -306.80 0.00 0.00 0.00
EMEA:Q3:LinuxCon EU 2016 -447.87 0.00 0.00 0.00
EMEA:Q3:OpenFest 2016 -246.90 0.00 0.00 0.00
EMEA:Q3:PyCon CZ 2016 -447.95 0.00 0.00 0.00
EMEA:Q3:SFK16 -185.94 0.00 0.00 0.00
EMEA:Q3:Swag -331.66 0.00 0.00 0.00
EMEA:Q3:nullCon -1598.40 0.00 0.00 0.00
EMEA:Q4 -4415.01 0.00 0.00 0.00
EMEA:Q4:34C3 0.00 -360.31 0.00 0.00
EMEA:Q4:Chaos Communication Congress -737.02 0.00 0.00 0.00
EMEA:Q4:DevConf 0.00 -376.74 0.00 0.00
EMEA:Q4:FOSDEM 2018 0.00 -4823.21 0.00 0.00
EMEA:Q4:Sfk16 -92.09 0.00 0.00 0.00
EMEA:Q4:Swag -1393.87 0.00 0.00 0.00
EMEA:Q4:Swag Production 0.00 -3547.73 0.00 0.00
EMEA:Q4:nullcon 2018 0.00 -974.22 0.00 0.00
EMEA:Unallocated -1624.25 0.00 0.00 0.00
Expense Expense:Airfare 41923.26 14519.17 9465.81 3647.31
Expense:Bus 115.14 0.00 0.00 0.00
Expense:Busines Meals 0.00 0.00 0.00 192.36
Expense:Business Meals 0.00 1703.66 34426.40 2185.19
Expense:Business Meals:Paypal 0.00 0.00 8.96 0.00
Expense:Business Meeting 0.00 0.00 3449.13 0.00
Expense:Conference 500.00 0.00 0.00 0.00
Expense:Conference Space 4137.00 0.00 0.00 0.00
Expense:Conference Supplies 125.00 0.00 0.00 0.00
Expense:Conference Ticket 193.19 0.00 0.00 0.00
Expense:Conference Tickets 67.00 0.00 0.00 0.00
Expense:Contributions/Donations 0.00 13000.00 0.00 0.00
Expense:Courier/Shipping 0.00 414.47 754.76 0.00
Expense:Currency Conversion Costs 187.13 0.00 0.00 0.00
Expense:Customs 0.00 48.89 0.00 0.00
Expense:Entertainment 20244.00 0.00 0.00 0.00
Expense:FUDPub 658.70 0.00 0.00 0.00
Expense:Finance Charge 1.86 0.00 0.00 0.00
Expense:Finance Charges 0.00 253.69 0.00 0.00
Expense:FinanceCharges:Paypal 155.32 0.00 0.00 0.00
Expense:Food 23070.06 0.00 0.00 0.00
Expense:Fuel 0.00 361.42 103.21 0.00
Expense:Hotel 867.20 0.00 0.00 0.00
Expense:Internet 2604.00 300.00 0.00 0.00
Expense:Lodging 12152.83 28842.02 26607.09 3273.27
Expense:Lodging:CC Intl Fee 3.20 0.00 0.00 0.00
Expense:Lodging:Deposit 0.00 0.00 0.00 0.00
Expense:Lodging:Paypal 13.56 0.00 0.00 0.00
Expense:Marketing 396.75 0.00 1018.79 0.00
Expense:Marketing Event 0.00 0.00 0.00 46681.00
Expense:Marketing:Others 157.12 0.00 0.00 0.00
Expense:Meals 560.04 21872.24 0.00 0.00
Expense:Meals:Bank 2.60 2.68 0.00 0.00
Expense:Meals:Paypal 3.82 22.11 0.00 0.00
Expense:Office Supplies 0.00 610.72 0.00 0.00
Expense:Other 219.67 46.65 0.00 0.00
Expense:Other Marketing Costs 280.86 1875.40 0.00 0.00
Expense:Other Marketing Costs:Bank 14.40 26.65 0.00 0.00
Expense:Other Marketing Costs:Banner 130.00 0.00 0.00 0.00
Expense:Other Marketing Costs:CC Intl Fee 3.18 0.00 0.00 0.00
Expense:Other Marketing Costs:Media 105.00 0.00 0.00 0.00
Expense:Other Marketing Costs:Paypal 21.18 39.39 0.00 0.00
Expense:Other Marketing Costs:Print 94.00 0.00 0.00 0.00
Expense:Other Marketing Costs:Stickers 518.05 0.00 0.00 0.00
Expense:Other Marketing Costs:Tshirt 62.00 0.00 0.00 0.00
Expense:Outreachy 6500.00 0.00 0.00 0.00
Expense:Outside Services/Training 0.00 3337.50 29474.35 0.00
Expense:Parking/Tolls 0.00 90.00 0.00 0.00
Expense:Pay Pal Fee 19.43 0.00 0.00 0.00
Expense:Paypal Fee 23.21 0.00 0.00 0.00
Expense:Paypal Fees 15.88 0.00 0.00 0.00
Expense:Postage 231.14 0.00 88.60 0.00
Expense:Printing/Reproduction 0.00 1248.97 46.79 0.00
Expense:PrintingReproduction 1800.68 0.00 0.00 0.00
Expense:Shipping 0.00 431.76 0.00 216.57
Expense:Supplies 0.00 0.00 127.23 0.00
Expense:Swag 3145.86 0.00 0.00 0.00
Expense:Swag:DVDs 4097.60 0.00 0.00 0.00
Expense:Swag:DVDs:PayPal Fee 51.20 0.00 0.00 0.00
Expense:Swag:Paypal Fee 14.84 0.00 0.00 0.00
Expense:Swag:Stickers 1050.53 0.00 0.00 0.00
Expense:T-Shirts 893.59 0.00 0.00 0.00
Expense:Taxi 129.10 0.00 0.00 28.15
Expense:Taxi/Car Service 0.00 125.07 513.32 0.00
Expense:Trade Show 1500.00 30167.79 74418.18 1012.03
Expense:Trade Show:Paypal Fee 58.80 11.56 0.00 0.00
Expense:TradeShow 123.36 0.00 0.00 0.00
Expense:Tradeshow 0.00 815.34 0.00 0.00
Expense:Train 45.42 67.27 1720.03 245.38
Expense:Transporation 0.00 200.19 0.00 0.00
Expense:Transport 5263.68 0.00 0.00 0.00
Expense:Transportation 234.07 67702.73 44280.64 0.00
Expense:Transportation:Agent Fee 0.00 1307.47 0.00 0.00
Expense:Transportation:Air 1700.06 829.29 0.00 0.00
Expense:Transportation:Bank 17.00 4.72 0.00 0.00
Expense:Transportation:Bus 203.50 82.66 0.00 0.00
Expense:Transportation:Paypal 20.60 48.72 5.20 0.00
Expense:Travel 21072.92 0.00 0.00 0.00
Expense:Travel:Paypal 12.06 0.00 0.00 0.00
Expense:Unallocated 762.00 0.00 0.00 0.00
Expense:Unknown 20354.80 0.00 0.00 0.00
Expense:Venue 1039.30 0.00 0.00 0.00
Expense:postage 100.00 0.00 0.00 0.00
Expense:ransportation 0.00 485.21 0.00 0.00
Expense:stamps.com 191.88 0.00 0.00 0.00
Expenses Expenses:Airfare 0.00 0.00 0.00 35412.35
Expenses:Business Meals 0.00 0.00 0.00 164.30
Expenses:Lodging 0.00 0.00 0.00 318.40
Expenses:Marketing Event 0.00 0.00 0.00 13084.92
Expenses:Services 0.00 0.00 0.00 933.00
Expenses:Trade Show 0.00 0.00 0.00 3081.18
Expenses:Train 0.00 0.00 0.00 58.03
Expenses:Transportation 0.00 0.00 0.00 163.60
External External:Flock:Copays 0.00 -5404.11 0.00 0.00
External:Flock:Other 0.00 -27.00 0.00 0.00
External:Flock:Registration 0.00 -3858.60 0.00 0.00
External:Flock:Sponsors 0.00 -5000.00 0.00 0.00
External:GSoC 0.00 -5700.00 0.00 0.00
External:LATAM DevConf Panam Refund 0.00 -1433.70 0.00 0.00
External:Red Hat 0.00 -196000.00 0.00 0.00
External:Refund 0.00 -85.17 0.00 0.00
FADs FADs:Cloud -2972.23 0.00 0.00 0.00
FADs:CommOps 0.00 -3742.96 0.00 0.00
FADs:Council 0.00 -2608.10 0.00 0.00
FADs:Design -4953.37 0.00 0.00 0.00
FADs:Diversity:Lodging -600.13 0.00 0.00 0.00
FADs:Diversity:Meals -197.89 0.00 0.00 0.00
FADs:Diversity:Travel -1657.06 0.00 0.00 0.00
FADs:Docs -4162.13 -9121.72 -888.39 0.00
FADs:G11N 0.00 -1716.20 0.00 0.00
FADs:G11N:Transportation 0.00 -5761.77 0.00 0.00
FADs:InfraHack 0.00 -7266.52 0.00 0.00
FADs:Infrastructure 0.00 0.00 -9948.08 0.00
FADs:LATAM:Airfare 0.00 -4811.85 0.00 0.00
FADs:LATAM:Food 0.00 -117.93 0.00 0.00
FADs:LATAM:Lodging 0.00 -451.13 0.00 0.00
FADs:LATAM:Travel 0.00 -17.29 0.00 0.00
FADs:Mindshare 0.00 0.00 -3482.19 0.00
FUDCon APAC FUDCon APAC -11669.91 0.00 0.00 0.00
FUDCon LATAM FUDCon LATAM -12197.90 0.00 0.00 0.00
Flock Flock -81283.79 -27.00 0.00 0.00
Flock:AV 0.00 -9502.59 -20564.41 -12137.04
Flock:Activities 0.00 -3471.00 0.00 0.00
Flock:Activities:Activity 0.00 0.00 -1758.83 -8829.26
Flock:Activities:Food 0.00 0.00 -6985.71 0.00
Flock:Activities:Transportion 0.00 0.00 -2874.33 0.00
Flock:Badges 0.00 -143.15 -330.90 -84.48
Flock:Bandwidth 0.00 -300.00 0.00 0.00
Flock:Booklet 0.00 -619.98 0.00 0.00
Flock:Copaid Travel 0.00 -5606.11 0.00 0.00
Flock:Food 0.00 -12912.18 -17195.30 -8390.00
Flock:Food:Sponsored Food 0.00 -4626.14 0.00 0.00
Flock:Funding:Bus 0.00 -1707.50 0.00 0.00
Flock:Funding:Lodging 0.00 -14207.23 -10309.37 -18842.40
Flock:Funding:Other 0.00 -2728.01 0.00 0.00
Flock:Funding:Transportation 0.00 0.00 -34965.91 -33704.33
Flock:Funding:Travel 0.00 -30897.89 0.00 0.00
Flock:Meeting Room 0.00 0.00 -3235.05 -9195.00
Flock:Paypal Fees 0.00 -188.19 0.00 0.00
Flock:Photographer 0.00 -3187.50 0.00 0.00
Flock:Schedule 0.00 -150.00 -500.00 0.00
Flock:Supplies 0.00 -760.43 -46.79 -287.43
Flock:T-shirt 0.00 -2398.00 -959.82 -2318.71
Flock:Video Editing 0.00 0.00 0.00 -933.00
LATAM LATAM:FY 18 Swag 0.00 0.00 -5834.17 0.00
LATAM:Q1 -321.23 0.00 0.00 0.00
LATAM:Q1:Cs50xni 0.00 -174.33 0.00 0.00
LATAM:Q1:F27 Release Party Managua 0.00 0.00 -60.85 0.00
LATAM:Q1:FISL -426.55 0.00 0.00 0.00
LATAM:Q1:FLISOL -50.89 0.00 0.00 0.00
LATAM:Q1:FLISOL Nicaragua 0.00 0.00 -88.60 0.00
LATAM:Q1:FLISOL Panama 0.00 0.00 -196.34 0.00
LATAM:Q1:Flisol 0.00 -1131.27 0.00 0.00
LATAM:Q1:InstallFest 0.00 -313.95 0.00 0.00
LATAM:Q1:LinuxPlaya 0.00 -605.24 0.00 0.00
LATAM:Q1:SASO Conf 0.00 -43.02 0.00 0.00
LATAM:Q2:ANDSEC -78.87 0.00 0.00 0.00
LATAM:Q2:CONECIT 0.00 -205.37 0.00 0.00
LATAM:Q2:FISL -1296.59 0.00 0.00 0.00
LATAM:Q2:FLISOL -222.66 0.00 0.00 0.00
LATAM:Q2:FSTTF -403.16 0.00 0.00 0.00
LATAM:Q2:Flock 0.00 -167.63 0.00 0.00
LATAM:Q2:Release Party -150.34 0.00 0.00 0.00
LATAM:Q2:Release Party Lima 0.00 0.00 -141.73 0.00
LATAM:Q2:Release Party Mexico 0.00 0.00 -51.00 0.00
LATAM:Q2:STFBA -87.10 0.00 0.00 0.00
LATAM:Q2:Zamora Teran 0.00 -27.82 0.00 0.00
LATAM:Q3:FGSL -120.99 0.00 0.00 0.00
LATAM:Q3:FTSL 0.00 -53.39 0.00 0.00
LATAM:Q3:Fedora Women Day - Cusco 0.00 -64.75 0.00 0.00
LATAM:Q3:Latinoware -268.40 -113.11 0.00 0.00
LATAM:Q3:Linux Day 0.00 -32.00 0.00 0.00
LATAM:Q3:LinuxUNI -845.78 0.00 0.00 0.00
LATAM:Q3:Peru Rumbo 0.00 -105.96 0.00 0.00
LATAM:Q3:STFBA -388.24 0.00 0.00 0.00
LATAM:Q3:Software Freedom Day 0.00 -203.21 0.00 0.00
LATAM:Q4:Devconf Panama -1558.80 0.00 0.00 0.00
LATAM:Q4:FGSL -20.15 0.00 0.00 0.00
LATAM:Q4:Swag 0.00 -2625.00 0.00 0.00
Mentored Projects Mentored Projects:Mentor Summit 0.00 0.00 0.00 -3022.93
Mindshare Mindshare:Advocate Events:Q1:Cambodia Translation Sprint 0.00 0.00 0.00 -90.25
Mindshare:Advocate Events:Q1:FLISoL Venezuela 0.00 0.00 0.00 -58.09
Mindshare:Advocate Events:Q1:Linux Edinburgh 0.00 0.00 0.00 -129.99
Mindshare:Advocate Events:Q2:CZ Release Parties 0.00 0.00 0.00 -329.62
Mindshare:Advocate Events:Q2:LFNW 0.00 0.00 0.00 -38.05
Mindshare:Advocate Events:Q2:Pune Release Party 0.00 0.00 0.00 -62.37
Mindshare:Advocate Events:Q2:Release Party Managua 0.00 0.00 0.00 -51.27
Mindshare:Advocate Events:Q3:FOSSCOMM 0.00 0.00 0.00 -747.42
Mindshare:FAD 0.00 -8662.94 0.00 0.00
Mindshare:Q1:Chemintzer Linux Tage 0.00 0.00 0.00 -350.82
Mindshare:Q1:LFNW 0.00 0.00 0.00 -65.96
Mindshare:Q1:OSCAL 0.00 0.00 0.00 -1113.20
Mindshare:Q1:SCALE 0.00 0.00 0.00 -1115.72
Mindshare:Q1:openSUSE 0.00 0.00 0.00 -233.35
Mindshare:Q3:Lisa 0.00 0.00 -238.95 0.00
Mindshare:Q3:SeaGL 0.00 0.00 -68.46 0.00
Mindshare:Q4:.NET 0.00 0.00 -146.32 0.00
Mindshare:Q4:DevConf.cz 0.00 0.00 -1296.05 0.00
Mindshare:Q4:FOSDEM 0.00 0.00 -5185.48 0.00
Mindshare:Q4:LFNW 0.00 0.00 -750.00 0.00
Mindshare:Q4:OSCAL 0.00 0.00 -1500.00 0.00
Mindshare:Q4:SELF 0.00 0.00 -500.00 0.00
Mindshare:Q4:Swag 0.00 0.00 -50.00 0.00
Mindshare:Q4:openSUSE 0.00 0.00 -28.04 0.00
Mindshare:Release Parties 0.00 0.00 -1219.06 0.00
Mindshare:Swag 0.00 0.00 0.00 -4093.21
NA NA:FY18 SWAG 0.00 0.00 -2849.07 0.00
NA:Q1 -509.34 0.00 0.00 0.00
NA:Q1:Bitcamp 0.00 -1209.16 0.00 0.00
NA:Q1:LFNW 0.00 -1565.72 0.00 0.00
NA:Q1:SCaLE15x 0.00 -1063.88 0.00 0.00
NA:Q1:SCaLE16X 0.00 0.00 -25.34 0.00
NA:Q1:SCaLE16x 0.00 0.00 -1370.78 0.00
NA:Q1:SELF 0.00 -500.00 0.00 0.00
NA:Q2 -6174.72 0.00 0.00 0.00
NA:Q2: Reimbursement 0.00 0.00 -26.34 0.00
NA:Q2:Bitcamp 0.00 -59.11 0.00 0.00
NA:Q2:Fosscon 0.00 -250.00 0.00 0.00
NA:Q2:HackMIT2017 0.00 -1045.26 0.00 0.00
NA:Q2:LFNW Reimbursement 0.00 0.00 -108.66 0.00
NA:Q2:LFNW/Scalex16 0.00 0.00 -76.51 0.00
NA:Q2:OSCAL 0.00 0.00 -400.45 0.00
NA:Q2:SE Linux Fest -352.68 0.00 0.00 0.00
NA:Q2:SEAGul -500.00 0.00 0.00 0.00
NA:Q2:SELF 0.00 -705.36 0.00 0.00
NA:Q2:Self 0.00 -186.64 0.00 0.00
NA:Q2:Southeast LF 0.00 0.00 -957.82 0.00
NA:Q3 -2531.28 0.00 0.00 0.00
NA:Q3:FOSSCON 0.00 -55.87 0.00 0.00
NA:Q3:Hackmit2017 0.00 -133.28 0.00 0.00
NA:Q3:LISA2017 0.00 -343.56 0.00 0.00
NA:Q3:Lisa2017 0.00 -558.91 0.00 0.00
NA:Q3:OLF 0.00 -1973.58 0.00 0.00
NA:Q3:Postage -155.64 0.00 0.00 0.00
NA:Q3:Shipping 0.00 0.00 -585.55 0.00
NA:Q3:Various 0.00 -431.76 0.00 0.00
NA:Q4 -6295.13 0.00 0.00 0.00
NA:Q4:Postage -75.50 0.00 0.00 0.00
NA:Unallocated -85.18 0.00 0.00 0.00
NA:stamps.com -293.43 0.00 0.00 0.00
Summer Coding Summer Coding:LDAP Conference 0.00 -902.58 0.00 0.00
Summer Coding:Mentor Summit 0.00 -2351.72 -2234.84 0.00
Show the code
import requests
import pandas as pd
from ledger_cli import LedgerParser

ledger_files = [
    ("FY20", "https://forge.rwright.co/rwright/fedora-budget/raw/branch/main/FY20/ledger/fedora.ldg"),
    ("FY19", "https://forge.rwright.co/rwright/fedora-budget/raw/branch/main/fy19/ledger/fedora.ldg"),
    ("FY18", "https://forge.rwright.co/rwright/fedora-budget/raw/branch/main/fy18/ledger/fedora.ldg"),
]

def load_ledger_to_df(fy, url):
    # Download to temp file
    tmp_path = f"/tmp/{fy}.ledger"
    with open(tmp_path, "w", encoding="utf-8") as f:
        f.write(requests.get(url).text)
    parser = LedgerParser(tmp_path)
    txns = parser.to_json()
    df = pd.json_normalize(txns)
    df["fy"] = fy
    return df

ledger_df = pd.concat([load_ledger_to_df(fy, url) for fy, url in ledger_files], ignore_index=True)
ledger_df.head()
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[14], line 22
     19     df["fy"] = fy
     20     return df
---> 22 ledger_df = pd.concat([load_ledger_to_df(fy, url) for fy, url in ledger_files], ignore_index=True)
     23 ledger_df.head()

Cell In[14], line 22, in <listcomp>(.0)
     19     df["fy"] = fy
     20     return df
---> 22 ledger_df = pd.concat([load_ledger_to_df(fy, url) for fy, url in ledger_files], ignore_index=True)
     23 ledger_df.head()

Cell In[14], line 17, in load_ledger_to_df(fy, url)
     15     f.write(requests.get(url).text)
     16 parser = LedgerParser(tmp_path)
---> 17 txns = parser.to_json()
     18 df = pd.json_normalize(txns)
     19 df["fy"] = fy

TypeError: LedgerParser.to_json() missing 1 required positional argument: 'data'