Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 9 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
[![CI](https://github.com/dev-belly/AuditLens/actions/workflows/ci.yml/badge.svg)](https://github.com/dev-belly/AuditLens/actions/workflows/ci.yml)
[![Python 3.11+](https://img.shields.io/badge/python-3.11%2B-blue.svg)](https://www.python.org/downloads/)
[![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)
[![Tests: 465](https://img.shields.io/badge/tests-465%20passing-brightgreen.svg)](#testing)
[![Tests: 501](https://img.shields.io/badge/tests-501%20passing-brightgreen.svg)](#testing)

**Financial anomaly detection and audit analytics over a 30,000-voucher general ledger.**

Expand Down Expand Up @@ -58,7 +58,7 @@ Three design commitments drive everything else:
| Anomalies caught by neither | **15** |
| Isolation Forest | ROC-AUC **0.816**, precision 0.291, recall 0.286 |
| Benford first-digit MAD | **0.00218** — close conformity |
| Test suite | **465 tests**, including dashboard render, warehouse and review-plan checks |
| Test suite | **501 tests**, including dashboard render, warehouse and review-plan checks |

### The most important number here is 98.2%, and it is a warning

Expand Down Expand Up @@ -565,7 +565,7 @@ AuditLens/
│ ├── components.py # KPI cards, risk badges, charts, tables
│ └── views/ # the six pages
├── sql/audit_queries.sql # 15 named business queries
├── tests/ # 465 tests, incl. cross-process reproducibility
├── tests/ # 501 tests, incl. cross-process reproducibility
├── docs/
│ ├── architecture.md # design decisions, data contracts, what is deliberately excluded
│ ├── methodology.md # every threshold, every weight, every mistake
Expand All @@ -579,7 +579,7 @@ AuditLens/
│ ├── notebook_content.py # what the three notebooks contain, kept apart from the builder
│ ├── capture_screenshots.py # headless captures of all six dashboard pages, via DevTools Protocol
│ └── ci_summary.py # headline figures for the CI job summary
├── .github/workflows/ci.yml # pipeline + 465 tests; Ubuntu 3.11/3.12 and Windows 3.12
├── .github/workflows/ci.yml # pipeline + 501 tests; Ubuntu 3.11/3.12 and Windows 3.12
└── Makefile
```

Expand Down Expand Up @@ -655,6 +655,8 @@ Stated plainly. A portfolio project that overclaims is worse than one that does

## Interview talking points

Chinese walkthrough: [Master-data integrity: business decisions, failure recovery and interview practice](docs/MASTER_DATA_CASE.zh-CN.md). The cleaner rejects conflicting supplier/employee IDs before lossy date parsing, and stages all four cleaning outputs before publication with rollback on handled failures. This is a single-writer CLI guarantee; concurrent snapshots and crash recovery require a separate versioning contract.

If you are reading this as a portfolio project, these are the things worth being able
to defend. Each is a real decision in the code.

Expand Down Expand Up @@ -750,19 +752,19 @@ builder, so two consecutive builds are byte-identical.
```bash
pip install -r requirements.txt
python src/run_pipeline.py # ~30 seconds, deterministic
python -m pytest tests/ -q # 465 tests
python -m pytest tests/ -q # 501 tests
```

Two consecutive runs produce byte-identical reports under `outputs/reports/`. This is
enforced by `tests/test_reproducibility.py`, not assumed.

## Testing

465 tests. `make test` runs the lot; `make test-fast` skips the dashboard render suite.
501 tests. `make test` runs the lot; `make test-fast` skips the dashboard render suite.

| File | Tests | What it pins down |
|---|---|---|
| `test_data_cleaning.py` | 57 | Each repair path: missing values, duplicates, invalid dates and amounts, debit/credit balance, orphan vendor IDs, inconsistent account names, currency normalisation |
| `test_data_cleaning.py` | 93 | Repairs, full-record master integrity, source-preserving conflicts, and staged cleaning publication with failure rollback |
| `test_audit_rules.py` | 38 | One class per procedure, plus mixed-size split-cluster regressions and the text-encoded-label fallback in `evaluate()` |
| `test_benford.py` | 56 | Expected frequencies, MAD bands, χ², the per-bucket z-score, and the schema stability of the insufficient-data branch |
| `test_risk_scoring.py` | 55 | Component scores, the noisy-OR combination, band boundaries, `risk_reasons` wording |
Expand Down
82 changes: 82 additions & 0 deletions docs/MASTER_DATA_CASE.zh-CN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# 面试案例:先证明主数据可信,再谈异常检测

本案例来自 AuditLens 的真实代码缺口与修复。数据是合成数据,项目是个人作品;没有真实客户上线、审计签字或收益证明。下面的第一人称讲法供理解代码后练习使用,个人分工应按事实改写。

## 60 秒讲法

> 我用 AuditLens 做了一条审计分析流程:清洗总账、执行规则、排序风险,再形成复核清单。我关注的一个问题是,规则本身正确,输入主数据却可能不可信。
>
> 原来同一个供应商编号出现两个银行账号时,清洗直接保留第一条。只要交换文件行顺序,选中的账号就变了。供应商关联和后续复核建立在这个结果上,所以这不是普通重复行问题。
>
> 修复是先按已有规则整理文本,再比较同一编号的所有源字段。完全一致的才合并;有冲突就停止,提示来源行号和字段,避免在日志里暴露账号。四个清洗结果也先暂存,发布失败会恢复旧结果。
>
> 我用交换输入顺序、空值、日期解析和中途写入失败来验证这个约束。它仍然是单进程分析工具;如果要支持历史主数据或并发读取,需要再设计版本和快照合同。

## 3 分钟讲法:把追问引向可以验证的业务决策

1. **业务链路。** 总账通过供应商、员工编号关联主数据。银行账号进入共享账号特征,员工角色进入职责分离例外的 SQL 复核结果。主数据必须先有一致的解释,否则后面漂亮的分数没有可靠基础。
2. **复现。** 构造两条 `vendor_id=V0001` 的记录,其他字段相同,账号分别为 `BANK_A` 和 `BANK_B`。旧实现按编号 `keep="first"`;逆序输入就得到另一个账号。问题不是选错了一个固定答案,而是系统没有选择答案的业务依据。
3. **选择。** 当前输入合同是“一次抽取中,每个编号对应一条一致记录”,没有生效区间、权威修订号或变更审批记录。因此不能擅自取最新、第一条、最后一条,也不能把空值自动补齐后假装记录一致。冲突需要回到来源核对。
4. **实现。** 先去除缺编号的记录,比较其余重复编号的所有字段。已知文本沿用空白归一化,空值彼此等价。检查发生在日期的有损解析之前,避免两个不同坏日期都变成 `NaT` 后被当成同一条。通过验证的供应商编号才用于孤立引用检查。
5. **失败处理。** 只有全部校验和四份序列化完成,才替换现有文件。发布到一半失败,恢复已替换的文件;第一次运行没有旧结果,则删除刚发布的部分结果。回滚也失败时保留旧内容备份,明确报告恢复路径。
6. **验证与边界。** 测试检查源数据没有被修改、冲突在两种顺序下都拒绝、第三份表或报告写失败不覆盖旧证据,并注入不同位置的替换失败。正常合成演示继续经过完整管线与 CI 检查。没有声称多文件替换对并发读者是一个原子快照,也没有声称可承受断电或进程被强杀。

## 当前合同:什么可以合并

| 输入差异 | 处理 | 理由 |
|---|---|---|
| 同一编号、所有字段一致 | 合并一份 | 不存在要裁决的属性差异 |
| 已知文本字段仅前后空白或重复空白不同 | 归一化后可合并 | 沿用项目已有文本清洗规则 |
| `None`、`NaN`、`pd.NA` | 视为同一缺失值 | 缺失标记的存储形式不构成业务差异 |
| 已知文本字段为空字符串或全空白 | 视为缺失 | 沿用现有空白语义 |
| 某字段缺失与该字段有值 | 拒绝 | 没有证据证明可以补齐成同一条 |
| 银行账号、角色或任何其他源字段不同 | 拒绝 | 不按白名单忽略新增字段 |
| 两个不同无效日期字符串 | 拒绝 | 不能用解析失败抹掉原始差异 |
| `2024-01-01` 与 `01/01/2024` | 拒绝 | 保守保留源格式差异,需上游统一日期合同 |
| 缺失或空白编号 | 去除 | 保持既有行为;本次未新增缺编号的治理报表 |

这是一项保守的输入校验。唯一编号记录的无效注册日期仍沿用原有解析为 `NaT` 的行为;本次没有把所有日期质量问题一并改成硬错误。

## 面试官追问与可落到代码的回答

**为什么不取注册日期最新的一条?** 注册日期是供应商属性,不是该记录的更新时间。把它用作版本顺序会编造业务规则。历史版本要有明确生效时间、失效时间或权威修订号,并约束同一时点只匹配一条。

**为什么比较所有列,连新增字段也检查?** 只比较名称和账号,未来新增风险状态或权限列时容易再次静默丢失差异。当前合同是完整记录一致;新增字段默认参与验证,若某列确实只是采集元信息,应先单独定义其合同。

**为什么空值不从另一条补齐?** 两条记录可能来自不同版本或不同系统。没有来源优先级时,自动补齐会组合出源文件里不存在的记录。缺失和有值先作为冲突交给上游确认。

**停止整条流程是不是影响可用性?** 是取舍:主数据在多个规则和关联中复用,继续运行可能输出错误证据。当前选择停止更新,保留上一次结果。上一次结果也不能当成最新结果;调用方要看到失败并核对输入批次。本项目尚未实现批次状态服务。

**你说原子写入,究竟原子在哪里?** 单个文件在同一文件系统内用替换操作发布;四份文件不是一个原子事务。实现保证正常捕获的失败触发回滚,不能保证并发读者在发布途中看不到混合版本。需要更强保证时,可以发布到独立版本目录,再切换一个清单入口,所有读者都依照该入口读取。

**如果回滚也因为权限或磁盘问题失败呢?** 报告“不完整回滚”,保留恢复旧内容所需的备份路径;不能继续报告成功。测试专门覆盖这个情况。文件备份也有清理失败风险,所以临时文件清理失败会记录日志。

**员工权限有没有真正的审批系统?** 没有。这是审计分析中的员工属性一致性校验。测试中的额外权限列说明所有源字段都参与比较,不代表项目实现了账号授权或审批权限执行。

**这是你自己做的还是 AI 帮你写的?** 按真实分工回答:“AI 协助了我实际使用到的部分;我能复现问题、解释为什么不能任意选一条,并现场修改和验证这条约束。”如果某部分还解释不清,应明确说明,先完成下面的练习,不能把生成代码的过程包装成客户经验。

## 现场复现与阅读路径

使用 README 中安装好的 Python 环境,在仓库根目录执行:

```shell
python -m pytest tests/test_data_cleaning.py -q -k "MasterDataIntegrity or CleaningPublication"
python -m pytest tests/test_data_cleaning.py -q
python src/run_pipeline.py
```

| 阅读入口 | 应解释的内容 |
|---|---|
| [`normalise_text`](../src/data_cleaning.py) | 哪些字段整理空白,为什么空白是缺失 |
| [`_deduplicate_master` / `clean_master_data`](../src/data_cleaning.py) | 全字段一致性、来源行号、日期解析顺序 |
| [`_publish_cleaning_outputs` / `run_cleaning`](../src/data_cleaning.py) | 校验、暂存、备份、发布、回滚的顺序 |
| [`TestMasterDataIntegrity`](../tests/test_data_cleaning.py) | 输入顺序、未列出的列、空值和源格式差异 |
| [`TestCleaningPublication`](../tests/test_data_cleaning.py) | 完整旧结果、首次运行、回滚失败的证据 |
| [`build_vendor_features`](../src/feature_engineering.py) / [SQL 审计查询](../sql/audit_queries.sql) | 银行账号与员工角色实际进入哪些分析结果 |
| [CI](../.github/workflows/ci.yml) | 跨平台测试、完整管线和报告复现检查 |

## 两个练习与解题方向

**练习一:添加 `supplier_status`,同一编号分别为 `active` / `blocked`。** 先预测结果,再修改测试数据并运行。解题方向:新增列也参与完整记录比较;异常应包含列名,不包含属性值。交换行序仍应拒绝。

**练习二:第三份文件发布失败,第一次运行和已有结果两种情况下磁盘应该是什么状态?** 不看实现先列出预期,再读替换失败测试。解题方向:已有结果的四份内容恢复原字节;首次运行四份都不存在;两种情况均清理暂存文件。追加“回滚自身失败”时,预期改为明确异常并保留恢复备份,不能继续声称结果一致。
Loading
Loading