Skip to main content

ledgerlens

Forensic accounting analytics for Python. Run Nigrini-style Benford tests, 13 explainable journal-entry procedures, Beneish / Altman / Piotroski financial screens, and export auditor-readable Excel workpapers. MIT licensed; Python 3.9–3.13.

ledgerlens surfaces red flags / reasons to investigate. A flag is a sample-selection signal, never a finding that fraud occurred.

Installation

pip install ledgerlens                 # core library v0.3.0 on PyPI
pip install 'ledgerlens[reference,plot]' # optional public R datasets and charts

PyPI releases · GitHub releases.

For development in a local checkout:

pip install -e '.[dev]'

LedgerLens Studio — no-code audit workbench (v0.3.0)

Install and launch the local web interface:

pip install 'ledgerlens[studio]'
ledgerlens-studio

The app opens on 127.0.0.1. Start with the synthetic demo, then upload a CSV/XLSX/Parquet general ledger. It detects English/Chinese/SAP-style fields, allows you to edit and save mappings, checks data quality before running JET01–13, and lets you filter exceptions, review entries, record investigation decisions and download the complete 16-sheet Excel workpaper. See Studio guide.

No API key or hosted AI is used. Uploaded financial files remain with the local Streamlit process. Review notes live in session memory until exported. No credentials or client documents are stored in the repository.

Smart Import: automatically find the ledger table

Upload an XLSX or CSV with title rows, multiple sheets or common SAP/Chinese column names. Studio ranks likely ledger worksheets and header rows, explains confidence, and allows manual override when two candidates are ambiguous. After approving the field mapping, download a normalized GL with source sheet and source row references. No AI API is required.

Template Migration: old workpaper → new format

In the left sidebar choose Template migration. Upload the previously completed workbook and a blank, redesigned XLSX template, or try the fictional demo. Two migration approaches are available:

  • Labeled fields and Named Ranges: match values by stable defined names and matching financial-report row labels; inspect/approve mappings, add manual cell mappings, and download the filled template and CSV reconciliation.
  • Tabular data: select each workbook's source/target sheet and header row, approve mapped columns, set a row limit and transfer into existing rows.

Every output starts with the supplied target template and preserves its layout, formatting, formulas and sheet names. Existing values are retained unless the user explicitly enables overwriting; formulas are always protected. There is no external data transfer. See Studio guide.

Benford's Law

import ledgerlens as ll

amounts = [119.20, 152.15, 203.00, 501.42, 509.10, 817.50]
result = ll.benford(amounts, test="first_two", sign="positive", min_value=None)
print(result.summary())
print(result.table.head())
print(result.suspects())

# Other methods: first, second, first_three, last_two, summation_test,
# number_duplication, second_order, mantissa_arc_test.

For meaningful conformity testing, use an appropriately sized population that spans several orders of magnitude. The library reports MAD, significance statistics, and record-level suspects, retaining source indexes.

Journal entry testing (JET01–JET13)

from ledgerlens.datasets import synthetic_gl
from ledgerlens.jet import JournalEntryTester

gl, truth = synthetic_gl(n_entries=2000, seed=0)  # SYNTHETIC teaching data
jet = JournalEntryTester(
    gl, entry_id="entry_id", date="post_date", amount="amount",
    account="account", description="description", user="user",
    effective_date="effective_date", period_end="2025-12-31",
)
report = jet.run_all(threshold=50000, limits=[50000])
print(report.summary().to_string(index=False))
print(report.entry_risk_scores().head(10))
print(report["JET09"].flagged[["entry_id", "description", "_reason"]].head())
report.to_excel("jet_report.xlsx")

Input rows represent GL lines. Debit amounts are positive and credits negative; alternatively provide debit="Debit" and credit="Credit" instead of amount=. Only entry identifier, posting date and an amount representation are mandatory. Optional account, user and narrative columns enable more tests. The 13 rules cover imbalance, weekends/holidays, round values, large values, amounts just below approval limits, duplicates, cutoff, backdating, English/Chinese risk keywords, blanks, rare accounts, rare users/off-hours and Benford excess-digit spikes. run_all() retains skipped rules and their reasons. Each flagged row has its original index and _reason; the supplied DataFrame is unchanged. Risk score = number of distinct rules hit.

The workbook has a linked Summary, sortable Entry risk, one tab per rule and a Benford tab with a native Excel bar chart when JET13 runs.

Financial scores

Single-company input uses a dict or pandas.Series and returns a dataclass; company-year batches use a DataFrame and return a DataFrame. All numbers in these examples are synthetic.

from ledgerlens import beneish_m, altman_z, piotroski_f

altman_inputs = dict(
    current_assets=40, current_liabilities=20, total_assets=100,
    retained_earnings=10, ebit=10, total_liabilities=50,
    market_cap=50, book_equity=50, sales=200,
)
print(altman_z(altman_inputs, model="public").summary())
print(altman_z(altman_inputs, model="private").summary())
print(altman_z(altman_inputs, model="non_manufacturing").summary())

beneish_fields = dict(
    receivables=(20, 10), sales=(200, 100), cogs=(100, 50),
    current_assets=(60, 30), ppe=(60, 30), total_assets=(200, 100),
    depreciation=(12, 6), sga=(20, 10), current_liabilities=(40, 20),
    long_term_debt=(60, 30),
)
beneish_inputs = {f"{name}_{period}": value
                  for name, pair in beneish_fields.items()
                  for period, value in zip(("t", "prev"), pair)}
beneish_inputs.update(net_income_t=20, cfo_t=10)
print(beneish_m(beneish_inputs).summary())  # 8-variable M8 and 5-variable M5

piotroski_inputs = dict(
    net_income_t=15, net_income_prev=5, cfo_t=20, cfo_prev=2,
    total_assets_t=100, total_assets_prev=100,
    long_term_debt_t=10, long_term_debt_prev=20,
    current_assets_t=60, current_assets_prev=40,
    current_liabilities_t=20, current_liabilities_prev=20,
    shares_outstanding_t=100, shares_outstanding_prev=100,
    sales_t=200, sales_prev=100, cogs_t=80, cogs_prev=50,
)
print(piotroski_f(piotroski_inputs).summary())  # F-Score: 0–9

Input names and published coefficients are documented in methodology; ratios with zero denominators become NaN.

Command line

ledgerlens demo -o demo.xlsx
ledgerlens benford payments.csv --column Amount --test first_two -o benford.xlsx
ledgerlens jet general_ledger.csv --entry-id JE --date PostDate --amount Amt \
  --account Account --description Description --user User --period-end 2025-12-31 \
  --threshold 100000 --limits 50000,100000 -o audit.xlsx
ledgerlens jet gl.xlsx --entry-id JE --date Date --debit Dr --credit Cr -o audit.xlsx
ledgerlens score beneish company_years.csv -o beneish.csv
ledgerlens score altman company_years.csv --model private -o altman.csv
ledgerlens score piotroski company_years.csv -o fscore.csv

CSV, XLSX and Parquet inputs are supported (Parquet requires an installed pandas Parquet engine). Successful commands exit 0; expected input/usage errors exit 2. ledgerlens demo generates its own synthetic GL and never fetches client data.

Reproducible validation (2026-11-10)

Check Independently measured result Method
CI matrix (3.9 / 3.11 / 3.13) 39 passed, 1 skipped on each version GitHub Actions: lint + coverage
Line coverage ~95% Regression suite, including Excel and timezone edge cases
Fresh installation PASS Isolated GitHub Actions venv; pip install . then ledgerlens demo -o demo.xlsx from /tmp; 16-sheet workbook
Synthetic JET recall 112/112 = 100.00% synthetic_gl(2000, seed=0); matching (entry_id, test_id) pairs
Clean-entry false-positive rate 390/2000 = 19.50% Clean entries flagged by at least one rule
Clean-entry-test false-positive rate 390/26000 = 1.50% Clean (entry_id, test_id) pairs flagged
1,000,000 GL lines / 13 JET rules 8.73 seconds Python 3.13.5, pandas 2.2.3; benchmarked separately in a local container
R corporate-payment first-two MAD 0.002336614 (matches 9 decimal places) Independent GitHub Actions network run, 185,083 eligible observations
R corporate-payment distortion factor −1.065467% (R) vs −1.064985% (corrected) Same public dataset, intentional denominator correction

The R reference test now runs in CI on push; locally it is opt-in using LEDGERLENS_NETWORK=1 pytest -m network -q after pip install '.[reference]'. Third-party .rda files are downloaded and cached at runtime, never bundled or committed.

Reproduced public case studies

Public reference dataset Eligible observations First-two MAD Conformity
Corporate payments 185,083 0.002336614 Nonconformity
Madoff exposure amounts 198 0.006075249 Nonconformity
Sino-Forest published figures 772 0.003463887 Nonconformity

Corporate payments Benford Madoff exposure Benford Sino-Forest Benford

Synthetic demonstration

Synthetic JET counts Synthetic entry risk

Reproduce case studies with python examples/case_studies.py --public (requires reference and plot extras). Generate the performance sample with python examples/benchmark_jet.py --lines 1000000. Interpret the synthetic false-positive rates as a controlled teaching example, not field precision. See public and synthetic case studies, methodology, performance, and decisions.

Development and release

python -m pytest -q
python -m pytest --cov=ledgerlens --cov-fail-under=90 -q
ruff check .
python -m build

CI runs Python 3.9, 3.11 and 3.13, also checking fresh installation, benchmark reproducibility and external R references. The GitHub v0.1.2 release includes a wheel and sdist. Both TestPyPI and PyPI v0.1.2 releases were successfully uploaded using Trusted Publishing (OIDC); subsequent releases use the manually triggered publication workflow. No persistent API tokens are stored.

Disclaimer and licence

These methods provide statistical indicators and audit sampling leads; conclusions require professional investigation, appropriate population selection, and corroborating evidence. Do not treat any output as an automated accusation of wrongdoing. MIT — see LICENSE.

Metadata

Release files for ledgerlens 0.3.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for ledgerlens 0.3.0
File Size Uploaded Provenance Actions
ledgerlens-0.3.0.tar.gz 71.8 kB With provenance Details for ledgerlens-0.3.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for ledgerlens 0.3.0
File Interpreter ABI Platform Actions
ledgerlens-0.3.0-py3-none-any.whl Python 3 none any Details for ledgerlens-0.3.0-py3-none-any.whl

Total release size: 131.3 kB

Release files / ledgerlens-0.3.0.tar.gz

Download URL ledgerlens-0.3.0.tar.gz
Size 71.8 kB
Tags Source
SHA-256 checksum
How to use checksums
3a83d8a872d5c8d58921626cda23c3d99d78f51600177ac1ac822b58fa200e50
BLAKE2b-256 checksum
How to use checksums
db6eb70446da2a47f71bc654934be54cfc20df56e491a8b14a9c1e95ad3dc0cc
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 10, 2026.

Transparency log

Release files / ledgerlens-0.3.0-py3-none-any.whl

Download URL ledgerlens-0.3.0-py3-none-any.whl
Size 59.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
7ce097a3a3799323852364267bffc525846d9aa21c587eb1dd229ea6162d0b36
BLAKE2b-256 checksum
How to use checksums
7ce95c942cf62a0921bfd5b8692a8a0f8f312455262326cf063867cabbf111ab
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 10, 2026.

Transparency log

Security for ledgerlens 0.3.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

Both files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for ledgerlens-0.3.0.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 10, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page