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.2.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.2.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.
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 |
Synthetic demonstration
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.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| ledgerlens-0.2.0.tar.gz | 56.4 kB | With provenance | Details for ledgerlens-0.2.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| ledgerlens-0.2.0-py3-none-any.whl | Python 3 | none | any | Details for ledgerlens-0.2.0-py3-none-any.whl |
Total release size: 102.1 kB
Release files / ledgerlens-0.2.0.tar.gz
| Download URL | ledgerlens-0.2.0.tar.gz |
|---|---|
| Size | 56.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
1ee3e4ec00f3084c2ce2d3fd06b068aee8b6b07ab18001ac24d6246ceadd7b82
|
|
BLAKE2b-256 checksum How to use checksums |
b3f28fddf81cd2dd25a7414a9f71b3e978051d970cc159c7bb9b840fbc2c1789
|
| 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 logRelease files / ledgerlens-0.2.0-py3-none-any.whl
| Download URL | ledgerlens-0.2.0-py3-none-any.whl |
|---|---|
| Size | 45.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
d9bd1185a322e4db58d58a84ba3a2aa8ee171532ddb33cac2aedaa0200f1e0d8
|
|
BLAKE2b-256 checksum How to use checksums |
a4128d8715b2a6ec69c02f14f8942aebfc4d429d2ec8364df1d6320eb0777199
|
| 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 logSecurity for ledgerlens 0.2.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.
Attestation for ledgerlens-0.2.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 logA 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.