Metadata-Version: 2.4
Name: ledgerlens
Version: 0.3.0
Summary: Forensic accounting analytics for Python: Benford's Law (Nigrini), journal entry testing, and Beneish / Altman / Piotroski scores.
Author: Yu Zonglin (Sam Yu)
License: MIT
Project-URL: Homepage, https://github.com/yuzonglinsam/ledgerlens
Project-URL: Issues, https://github.com/yuzonglinsam/ledgerlens/issues
Keywords: audit,forensic accounting,benford,journal entry testing,fraud detection,beneish,altman,piotroski,data analytics
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Financial and Insurance Industry
Classifier: Intended Audience :: Science/Research
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Office/Business :: Financial :: Accounting
Classifier: Topic :: Scientific/Engineering :: Information Analysis
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: numpy>=1.21
Requires-Dist: pandas>=1.3
Requires-Dist: scipy>=1.7
Requires-Dist: openpyxl>=3.0
Provides-Extra: excel
Requires-Dist: openpyxl>=3.0; extra == "excel"
Provides-Extra: plot
Requires-Dist: matplotlib>=3.4; extra == "plot"
Provides-Extra: reference
Requires-Dist: pyreadr>=0.4; extra == "reference"
Provides-Extra: studio
Requires-Dist: streamlit<2,>=1.35; extra == "studio"
Provides-Extra: all
Requires-Dist: openpyxl>=3.0; extra == "all"
Requires-Dist: matplotlib>=3.4; extra == "all"
Requires-Dist: pyreadr>=0.4; extra == "all"
Provides-Extra: dev
Requires-Dist: pytest>=7; extra == "dev"
Requires-Dist: pytest-cov>=4; extra == "dev"
Requires-Dist: ruff>=0.6; extra == "dev"
Requires-Dist: openpyxl>=3.0; extra == "dev"
Requires-Dist: matplotlib>=3.4; extra == "dev"
Dynamic: license-file

# 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

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

[PyPI releases](https://pypi.org/project/ledgerlens/) ·
[GitHub releases](https://github.com/yuzonglinsam/ledgerlens/releases).

For development in a local checkout:

```bash
pip install -e '.[dev]'
```

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

Install and launch the local web interface:

```bash
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](docs/studio.md).

**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](docs/studio.md).

## Benford's Law

```python
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)

```python
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*.

```python
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](docs/methodology.md); ratios with zero denominators become NaN.

## Command line

```bash
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](docs/figures/corporate_payment-benford.png)
![Madoff exposure Benford](docs/figures/madoff_returns-benford.png)
![Sino-Forest Benford](docs/figures/sino_forest-benford.png)

### Synthetic demonstration

![Synthetic JET counts](docs/figures/synthetic-jet-counts.png)
![Synthetic entry risk](docs/figures/synthetic-entry-risk.png)

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](docs/case-studies.md),
[methodology](docs/methodology.md), [performance](docs/performance.md), and
[decisions](docs/decisions.md).

## Development and release

```bash
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](https://github.com/yuzonglinsam/ledgerlens/releases/tag/v0.1.2) includes a wheel and sdist.
Both TestPyPI and PyPI v0.1.2 releases were successfully uploaded using
[Trusted Publishing (OIDC)](docs/publishing.md); 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](LICENSE).
