Ledger generation: transactions, running balances & statements
This section demonstrates the detail → derive pattern on the archetypal hard case, a bank ledger: transactions generated on an account x day x sequence scaffold, calendar-driven behavior (salary on the 25th, card-heavy weekends), signs and per-account running balances computed exactly via sequence-aware calculated features, and a monthly statement derived from the ledger — closing balances and totals reconciling by construction.
Example Code
"""
Example: bank-ledger generation — transactions with exact running balances,
and monthly statements derived from them.
The full FakerSynthesizer pipeline on the archetypal hard case:
- accounts reference table: opening balances and segments carried into the
rows via scaffold `include` (joined, never invented)
- scaffold: account x day x intra-day sequence — every account active every
day, volumes fixed by construction
- conditions with CALENDAR PREDICATES: salary credits cluster on the 25th,
card activity rises at weekends; amounts differ by category and segment
- sequence + per-partition calculated features: the running balance is
opening_balance + cumsum(signed_amount) PER ACCOUNT, in order — exact to
the penny on every row
- derived monthly statement: one row per account, with `last`-aggregated
closing balance — reconciling with the ledger by construction.
Deterministic, reproducible under random_state, zero LLM calls.
"""
import pandas as pd
from ydata.metadata import Metadata
from ydata.metadata.builder import MetadataConfigurationBuilder
from ydata.synthesizers import FakerSynthesizer
TRANSACTIONS_PER_ACCOUNT_PER_DAY = 4
MONTH_START, MONTH_END = "2026-03-01", "2026-03-31"
# ---------------------------------------------------------------------------
# Accounts reference (in practice: read from your master)
# ---------------------------------------------------------------------------
accounts = pd.DataFrame({
"account_id": ["ACC-1001", "ACC-1002", "ACC-1003", "ACC-1004"],
"segment": ["retail", "retail", "business", "business"],
"opening_balance": [2_500.00, 830.50, 40_000.00, 12_750.25],
})
CREDIT_CATEGORIES = {"salary", "incoming_transfer"}
columns = {
"category": {"datatype": "categorical", "vartype": "string",
"categories": {"card_payment": 45, "groceries": 20, "utilities": 10,
"outgoing_transfer": 10, "incoming_transfer": 10, "salary": 5}},
"amount": {"datatype": "numerical", "vartype": "float", "min": 1.0, "max": 100_000.0},
}
metadata = Metadata(configuration_builder=MetadataConfigurationBuilder(columns))
synth = FakerSynthesizer(locale="en")
synth.fit(
metadata,
table_name="ledger",
scaffold={
"dimensions": [
{"column": "account_id", "from_table": "accounts",
"from_column": "account_id", "include": ["segment", "opening_balance"]},
{"column": "booking_date",
"date_range": {"start": MONTH_START, "end": MONTH_END, "freq": "D"}},
{"column": "entry_seq",
"values": list(range(1, TRANSACTIONS_PER_ACCOUNT_PER_DAY + 1))},
]
},
reference_data={"accounts": accounts},
sequence={"order_by": ["booking_date", "entry_seq"], "partition_by": ["account_id"]},
conditions=[
{ # payday: salary credits cluster on the 25th; weekends are card-heavy
"column": "category",
"rules": [
{"when": {"booking_date": {"day_in_month": [25]}},
"values": {"salary": 0.5, "card_payment": 0.3, "groceries": 0.2}},
{"when": {"booking_date": {"weekday_in": [5, 6]}},
"values": {"card_payment": 0.6, "groceries": 0.3, "incoming_transfer": 0.1}},
],
# weekdays fall back to the declared category distribution
},
{ # amount magnitude per category and segment
"column": "amount",
"rules": [
{"when": {"category": "salary"},
"distribution": {"distribution": "normal", "loc": 3200.0, "scale": 400.0, "min": 1200.0}},
{"when": {"category": ["incoming_transfer", "outgoing_transfer"],
"segment": "business"},
"distribution": {"distribution": "lognormal", "mean": 8.0, "sigma": 0.8}},
{"when": {"category": ["incoming_transfer", "outgoing_transfer"]},
"distribution": {"distribution": "lognormal", "mean": 5.5, "sigma": 0.9}},
{"when": {"category": "utilities"},
"distribution": {"distribution": "normal", "loc": 120.0, "scale": 30.0, "min": 20.0}},
],
"default": {"distribution": {"distribution": "lognormal", "mean": 3.4, "sigma": 0.8}},
},
],
calculated_features=[
{ # sign follows the category — credits in, debits out; a rule, exact
"calculated_features": "signed_amount",
"function": lambda amount, category: (
amount * category.isin(CREDIT_CATEGORIES).map({True: 1, False: -1})
).round(2),
"calculated_from": ["amount", "category"]},
{ # the running balance: per account, in order, exact to the penny
"calculated_features": "balance",
"function": lambda opening, signed: (opening + signed.cumsum()).round(2),
"calculated_from": ["opening_balance", "signed_amount"],
"per_partition": True},
],
derived_tables=[
{"name": "monthly_statement", "type": "aggregate",
"group_by": ["account_id", "segment", "opening_balance"],
"aggregations": {
"n_transactions": {"column": "signed_amount", "agg": "count"},
"total_movement": {"column": "signed_amount", "agg": "sum"},
"closing_balance": {"column": "balance", "agg": "last"}, # order-aware
}},
],
)
data = synth.sample(random_state=42)
ledger = data["ledger"].to_pandas()
statement = data["monthly_statement"].to_pandas()
# ---------------------------------------------------------------------------
# The checks an accountant would run
# ---------------------------------------------------------------------------
# running balance exact on every row, per account, in order
for _, account_rows in ledger.groupby("account_id"):
expected = (account_rows["opening_balance"].iloc[0]
+ account_rows["signed_amount"].cumsum()).round(2)
assert (account_rows["balance"] == expected).all()
# the statement reconciles with the ledger by construction
merged = statement.set_index("account_id")
assert (merged["closing_balance"]
== (merged["opening_balance"] + merged["total_movement"]).round(2)).all()
assert merged["n_transactions"].sum() == len(ledger)
# calendar behavior: salary concentrates on the 25th
dates = pd.to_datetime(ledger["booking_date"])
salary_share_payday = (ledger[dates.dt.day == 25]["category"] == "salary").mean()
salary_share_rest = (ledger[dates.dt.day != 25]["category"] == "salary").mean()
assert salary_share_payday > salary_share_rest * 3
print(ledger.head(8).to_string())
print(f"\n{len(ledger)} ledger entries | {len(statement)} monthly statements")
print(statement.to_string(index=False))
print("\nAll balance, reconciliation and calendar checks passed.")