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 .                         # from a cloned repository; includes Excel support
pip install '.[dev]'                 # development, pytest, pytest-cov and ruff
pip install '.[reference,plot]'      # optional R reference downloader and visualisations

Download the verified v0.1.2 wheel and source distribution. The package is not on PyPI yet; pip install ledgerlens will become available after PyPI Trusted Publishing is configured and the upload succeeds.

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. A manual OIDC publishing workflow is ready for TestPyPI and PyPI; the repository owner must register the Trusted Publisher and explicitly initiate publication.

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.1.2

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.1.2
File Size Uploaded Provenance Actions
ledgerlens-0.1.2.tar.gz 42.5 kB With provenance Details for ledgerlens-0.1.2.tar.gz

Built distribution (wheel)

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

Total release size: 76.5 kB

Release files / ledgerlens-0.1.2.tar.gz

Download URL ledgerlens-0.1.2.tar.gz
Size 42.5 kB
Tags Source
SHA-256 checksum
How to use checksums
8c3c482564072eef22c6fb771a0c8fff243cb3a2f73698958b6bf1280576ede8
BLAKE2b-256 checksum
How to use checksums
8dab2ecb14d0cc4d9a8ecf63369233f0f51b81f4130ba38460356b2f11253caa
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.1.2-py3-none-any.whl

Download URL ledgerlens-0.1.2-py3-none-any.whl
Size 33.9 kB
Tags Python 3
SHA-256 checksum
How to use checksums
9bc1f5a21ce2d35c2462b8bb04705b0040e52e48b6532308f680c6d1fe0ad56a
BLAKE2b-256 checksum
How to use checksums
75c67a472f23d79106ee8711f47502663be32362760aa2bc0ea0c49687661036
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.1.2

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.1.2.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