From 4b262de009a229c2eacfc189ef892f46acfec227 Mon Sep 17 00:00:00 2001 From: dev-belly Date: Sun, 18 Oct 2026 10:31:21 +0800 Subject: [PATCH] docs: add an executable browser trial for invoice revisions --- .github/workflows/ci.yml | 4 + README.md | 6 +- README.zh-CN.md | 5 +- docs/SHARE.md | 7 ++ examples/README.md | 8 ++ examples/decision_time.ipynb | 237 +++++++++++++++++++++++++++++++++++ scripts/check_notebook.py | 107 ++++++++++++++++ 7 files changed, 372 insertions(+), 2 deletions(-) create mode 100644 examples/decision_time.ipynb create mode 100644 scripts/check_notebook.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9ca6ff8..9b5f8aa 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,6 +24,8 @@ jobs: run: python -m unittest discover -s tests -v - name: Run the Python library example run: python examples/decision_time.py + - name: Run notebook cells against the current package without the pinned install cell + run: python scripts/check_notebook.py - name: Verify the scale runner on a small deterministic workload run: pitbridge benchmark --rows 2000 --repeats 2 --out outputs/scaling-ci - name: Replay committed evidence @@ -83,6 +85,8 @@ jobs: if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } .\.venv\Scripts\python.exe examples/decision_time.py if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + .\.venv\Scripts\python.exe scripts/check_notebook.py + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } .\.venv\Scripts\python.exe -m pitbridge demo --out outputs if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } .\.venv\Scripts\python.exe -m pitbridge verify --out outputs diff --git a/README.md b/README.md index 93fd9a1..5269305 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,11 @@ **Financial features as they were known when a decision was made.** [![CI](https://github.com/dev-belly/PITBridge/actions/workflows/ci.yml/badge.svg)](https://github.com/dev-belly/PITBridge/actions/workflows/ci.yml) -[Try one invoice](examples/README.md#a-correction-that-arrives-later) · [Bring CSV tables](examples/README.md#optional-pandascsv-input) · [Open the online report](https://dev-belly.github.io/PITBridge/) · [中文说明](README.zh-CN.md) +[Run in Colab](https://colab.research.google.com/github/dev-belly/PITBridge/blob/main/examples/decision_time.ipynb) · [Try one invoice locally](examples/README.md#a-correction-that-arrives-later) · [Bring CSV tables](examples/README.md#optional-pandascsv-input) · [Open the online report](https://dev-belly.github.io/PITBridge/) · [中文说明](README.zh-CN.md) + +The notebook installs **dev-belly/PITBridge** from a full source URL pinned to one +commit; no local checkout is needed. Change a decision or ingestion time to see +which invoice revision remains available. [Methodology](docs/METHODOLOGY.md) · [Data contract](docs/DATA_CONTRACT.md) · [Interview notes](docs/INTERVIEW.md) diff --git a/README.zh-CN.md b/README.zh-CN.md index a27ebf7..fb5bd52 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -4,7 +4,10 @@ **核心问题:做出信贷决策时,银行当时到底能看到哪个版本的数据?** -[先试一张发票](examples/README.md#a-correction-that-arrives-later) · [从 CSV 表格开始](examples/README.md#optional-pandascsv-input) · [在线报告](https://dev-belly.github.io/PITBridge/) · [提交试用反馈](https://github.com/dev-belly/PITBridge/issues/new?template=trial_feedback.md) +[浏览器里试一张发票(Colab)](https://colab.research.google.com/github/dev-belly/PITBridge/blob/main/examples/decision_time.ipynb) · [本地案例](examples/README.md#a-correction-that-arrives-later) · [从 CSV 表格开始](examples/README.md#optional-pandascsv-input) · [在线报告](https://dev-belly.github.io/PITBridge/) · [提交试用反馈](https://github.com/dev-belly/PITBridge/issues/new?template=trial_feedback.md) + +Notebook 使用 **dev-belly/PITBridge** 的完整源码 URL,固定到一个提交版本,无需克隆。 +修改决策或入库时间,即可查看当时应选择哪个发票版本。 例如,一月份开票金额在二月份公开、三月份才进入银行系统,四月份又被修订。直接按照“一月份”去关联历史申请,会把后来的信息放进早期特征,导致回测过于乐观。 diff --git a/docs/SHARE.md b/docs/SHARE.md index 690411b..5a69745 100644 --- a/docs/SHARE.md +++ b/docs/SHARE.md @@ -1,5 +1,12 @@ # Share the invoice counterexample +For a browser trial, [open the editable invoice notebook in Colab](https://colab.research.google.com/github/dev-belly/PITBridge/blob/main/examples/decision_time.ipynb): +no local clone is needed. Its full installation URL selects dev-belly/PITBridge +at a fixed source commit. All records are synthetic. + +也可直接[在浏览器里改两个时间、试一次发票选择](https://colab.research.google.com/github/dev-belly/PITBridge/blob/main/examples/decision_time.ipynb), +无需本地克隆;Notebook 通过完整源码 URL 安装固定提交版本,记录均为合成数据。 + ## 中文 做金融数据或特征工程时,事后修订容易被误用到历史决策。我做了 [PITBridge](https://github.com/dev-belly/PITBridge):用合成发票说明,同为2026年1月31日数据,v1于2月5日发布、6日入库,值10万元;v2于4月1日发布、3日入库,值13.5万元。90天新鲜度下,2月1日未可用,2月15日选v1,4月15日选v2;SQL与独立Python逐行核对。[小示例](https://github.com/dev-belly/PITBridge/blob/main/examples/README.md)可本地运行;[在线演示](https://dev-belly.github.io/PITBridge/)展示11条观测、45次查询的大案例。想请你试试CSV输入,反馈时点与修订选择是否贴合你的场景:[提交反馈](https://github.com/dev-belly/PITBridge/issues/new?template=trial_feedback.md)。 diff --git a/examples/README.md b/examples/README.md index e026f42..7a9776a 100644 --- a/examples/README.md +++ b/examples/README.md @@ -1,5 +1,13 @@ # Small, runnable examples +Want to edit a timestamp without setting up a local checkout? +[Open the invoice notebook in Colab](https://colab.research.google.com/github/dev-belly/PITBridge/blob/main/examples/decision_time.ipynb) +or [inspect its source](decision_time.ipynb). It installs dev-belly/PITBridge from +a full source URL pinned to one commit, checks the three fixed decisions, and +then lets you change decision and correction-ingestion times. Only the standard +library is used at runtime; installation needs network access. The separate +editable scenario does not reuse the fixed fixture's expected results. + Install the repository in a Python 3.11+ virtual environment using the [quick start](../README.md#run-in-one-minute), then run: diff --git a/examples/decision_time.ipynb b/examples/decision_time.ipynb new file mode 100644 index 0000000..5bb10d6 --- /dev/null +++ b/examples/decision_time.ipynb @@ -0,0 +1,237 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "intro", + "metadata": {}, + "source": [ + "# Which invoice revision could this decision actually know?\n", + "\n", + "One synthetic invoice, two revisions, three decision dates. Compare a deliberately\n", + "wrong event-date join with availability-aware financial / ML features, then edit\n", + "two timestamps yourself. **These invented CNY amounts demonstrate a data contract;\n", + "they do not establish model accuracy, investment returns or lending outcomes.**\n", + "\n", + "一张合成发票,两个修订版本:一月份发生的数据,二月份和四月份的决策分别能看到什么?\n", + "\n", + "Use Python 3.11+ and **Run all** in a fresh runtime. No clone, GPU, dataset account\n", + "or third-party runtime library is needed. Installation requires network access;\n", + "the example then runs locally in the notebook. Colab sign-in follows Google's\n", + "platform requirements." + ] + }, + { + "cell_type": "markdown", + "id": "install-notes", + "metadata": {}, + "source": [ + "## 1. Install this exact source snapshot\n", + "\n", + "The full URL below selects **dev-belly/PITBridge**, pinned to commit\n", + "`a5e423debda20555a0cef4500606c9533908c6fd`. Use this source URL to identify this\n", + "project rather than relying on a package name alone. Pip may download build tools;\n", + "PITBridge itself has no third-party runtime dependencies." + ] + }, + { + "cell_type": "code", + "id": "install-source", + "metadata": { + "tags": [ + "pitbridge-install" + ] + }, + "source": [ + "import subprocess\n", + "import sys\n", + "\n", + "if sys.version_info < (3, 11):\n", + " raise RuntimeError(\"This example requires Python 3.11 or newer\")\n", + "\n", + "# Exact source owner and commit, without Git or a source checkout.\n", + "SOURCE_COMMIT = \"a5e423debda20555a0cef4500606c9533908c6fd\"\n", + "SOURCE_URL = f\"https://codeload.github.com/dev-belly/PITBridge/zip/{SOURCE_COMMIT}\"\n", + "subprocess.check_call([\n", + " sys.executable, \"-m\", \"pip\", \"install\", \"--quiet\",\n", + " \"--disable-pip-version-check\", \"--force-reinstall\", \"--no-deps\", SOURCE_URL,\n", + "])\n", + "print(f\"Installed dev-belly/PITBridge from {SOURCE_COMMIT}\")" + ], + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "id": "invoice-contract", + "metadata": {}, + "source": [ + "## 2. Check the three original decisions\n", + "\n", + "Both records describe January 31. The original amount is published February 5\n", + "and ingested February 6; the correction is published April 1 and ingested April 3.\n", + "Availability is **the later of publication and ingestion**, with a 90-day\n", + "freshness limit. The hand-calculated expectations are:\n", + "\n", + "| Decision (UTC) | Expected result |\n", + "| --- | --- |\n", + "| February 1 | missing: `not_available` |\n", + "| February 15 | `invoice-v1`, CNY 100,000 |\n", + "| April 15 | `invoice-v2`, CNY 135,000 |\n", + "\n", + "SQL must match both these fixed expectations and the independent Python\n", + "enumerator. These assertions belong to this unchanged fixture; the editable\n", + "scenario appears separately below." + ] + }, + { + "cell_type": "code", + "id": "fixed-expectations", + "metadata": {}, + "source": [ + "import sys\n", + "import pitbridge\n", + "from pitbridge import Decision, FeatureSpec, Observation, build_snapshot, reference_snapshot\n", + "from pitbridge.bundle import compare_unsafe\n", + "\n", + "print(f\"Python {sys.version.split()[0]}; PITBridge imported from {pitbridge.__file__}\")\n", + "\n", + "def invoice_observations(correction_ingested_at=\"2026-04-03T00:00:00Z\"):\n", + " return [\n", + " Observation(\n", + " record_id=\"invoice-v1\", entity_id=\"SME-001\", source=\"tax\",\n", + " feature=\"invoice_revenue\", event_at=\"2026-01-31T00:00:00Z\",\n", + " published_at=\"2026-02-05T00:00:00Z\", ingested_at=\"2026-02-06T00:00:00Z\",\n", + " revision=1, value=100000,\n", + " ),\n", + " Observation(\n", + " record_id=\"invoice-v2\", entity_id=\"SME-001\", source=\"tax\",\n", + " feature=\"invoice_revenue\", event_at=\"2026-01-31T00:00:00Z\",\n", + " published_at=\"2026-04-01T00:00:00Z\", ingested_at=correction_ingested_at,\n", + " revision=2, value=135000,\n", + " ),\n", + " ]\n", + "\n", + "observations = invoice_observations()\n", + "decisions = [\n", + " Decision(\"01-before-arrival\", \"SME-001\", \"2026-02-01T00:00:00Z\"),\n", + " Decision(\"02-original-known\", \"SME-001\", \"2026-02-15T00:00:00Z\"),\n", + " Decision(\"03-correction-known\", \"SME-001\", \"2026-04-15T00:00:00Z\"),\n", + "]\n", + "specs = [FeatureSpec(\"tax\", \"invoice_revenue\", max_age_days=90)]\n", + "snapshots = build_snapshot(observations, decisions, specs)\n", + "assert snapshots == reference_snapshot(observations, decisions, specs)\n", + "expected = [\n", + " (\"01-before-arrival\", None, \"not_available\", None),\n", + " (\"02-original-known\", 100000.0, \"selected\", \"invoice-v1\"),\n", + " (\"03-correction-known\", 135000.0, \"selected\", \"invoice-v2\"),\n", + "]\n", + "actual = [(row.decision_id, row.value, row.status, row.record_id) for row in snapshots]\n", + "assert actual == expected, (actual, expected)\n", + "print(\"All three hand-calculated decisions match SQL and independent Python replay.\")" + ], + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "id": "unsafe-baseline", + "metadata": {}, + "source": [ + "## 3. See what the wrong join does\n", + "\n", + "This is the same deliberately unsafe comparator used in the saved PITBridge\n", + "report. It filters only on event date and takes the highest revision, ignoring\n", + "availability, tombstones and freshness. **It is a counterexample, never a\n", + "feature-building recommendation.** Its April correction leaks into both February\n", + "decisions, even though all records share the same January event date." + ] + }, + { + "cell_type": "code", + "id": "compare-selections", + "metadata": {}, + "source": [ + "def show_comparison(source, rows):\n", + " comparison = compare_unsafe(source, rows)\n", + " print(\"Decision UTC | Wrong event-only record/value | Availability-aware result | Future knowledge?\")\n", + " for row, item in zip(rows, comparison, strict=True):\n", + " wrong = f\"{item['unsafe_record_id']}: {item['unsafe_value']}\"\n", + " safe = f\"{row.status}, {row.record_id}: {row.value}\"\n", + " print(f\"{row.decision_at[:10]} | {wrong} | {safe} | {item['unsafe_uses_future_knowledge']}\")\n", + " return comparison\n", + "\n", + "comparison = show_comparison(observations, snapshots)\n", + "assert [row[\"unsafe_record_id\"] for row in comparison] == [\"invoice-v2\"] * 3\n", + "assert [row[\"unsafe_uses_future_knowledge\"] for row in comparison] == [True, True, False]\n", + "print(\"The wrong join uses future knowledge in 2 of these 3 invented decisions.\")" + ], + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "id": "edit-time-notes", + "metadata": {}, + "source": [ + "## 4. Change two timestamps and rerun just the next cell\n", + "\n", + "Try moving `decision_at` from April 2 to April 4, or move\n", + "`correction_ingested_at` from April 3 to April 1. Use timezone-aware ISO strings\n", + "(`Z` means UTC). The correction's publication date remains April 1: an earlier\n", + "ingestion timestamp cannot make it available before publication.\n", + "\n", + "This separate scenario checks SQL against the independent enumerator. It does\n", + "not reuse the original fixture's fixed expectations, so valid edits can change\n", + "the result. The core contract will still reject invalid input timestamps." + ] + }, + { + "cell_type": "code", + "id": "editable-times", + "metadata": {}, + "source": [ + "# Edit either string, then rerun this cell.\n", + "decision_at = \"2026-04-02T00:00:00Z\"\n", + "correction_ingested_at = \"2026-04-03T00:00:00Z\"\n", + "\n", + "custom_observations = invoice_observations(correction_ingested_at)\n", + "custom_decisions = [Decision(\"editable-decision\", \"SME-001\", decision_at)]\n", + "custom_rows = build_snapshot(custom_observations, custom_decisions, specs)\n", + "assert custom_rows == reference_snapshot(custom_observations, custom_decisions, specs)\n", + "print(f\"Correction available at: {custom_observations[1].available_at}\")\n", + "custom_comparison = show_comparison(custom_observations, custom_rows)" + ], + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "id": "next-steps", + "metadata": {}, + "source": [ + "## Apply the timing question to your own feature pipeline\n", + "\n", + "What timestamp in your feed establishes when a revision was genuinely available?\n", + "This notebook does not authenticate that upstream timestamp or demonstrate a\n", + "real trading or credit outcome. Read the [data contract](https://github.com/dev-belly/PITBridge/blob/main/docs/DATA_CONTRACT.md)\n", + "before adapting the API. For existing tables, the [optional CSV example](https://github.com/dev-belly/PITBridge/blob/main/examples/README.md#optional-pandascsv-input)\n", + "is a separate local adapter requiring pandas.\n", + "\n", + "If a small synthetic counterexample exposes a mismatch with your workflow,\n", + "[share the timestamps, expected selection and observed result](https://github.com/dev-belly/PITBridge/issues/new?template=trial_feedback.md)." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/scripts/check_notebook.py b/scripts/check_notebook.py new file mode 100644 index 0000000..797cb0b --- /dev/null +++ b/scripts/check_notebook.py @@ -0,0 +1,107 @@ +"""Validate the saved first-trial notebook and execute its ordinary Python cells.""" + +from __future__ import annotations + +import argparse +import importlib.util +import json +import re +import sys +from pathlib import Path + +INSTALL_TAG = "pitbridge-install" +ROOT = Path(__file__).resolve().parents[1] + + +def read_notebook(path: Path) -> list[dict]: + notebook = json.loads(path.read_text(encoding="utf-8")) + if notebook.get("nbformat") != 4 or notebook.get("nbformat_minor") != 5: + raise ValueError("Expected notebook format 4.5") + cells = notebook.get("cells") + if not isinstance(cells, list) or not cells: + raise ValueError("Expected a nonempty notebook cell list") + identifiers = set() + code_cells = [] + for cell in cells: + identity = cell.get("id", "") + if not isinstance(identity, str) or not re.fullmatch( + r"[A-Za-z0-9_-]{1,64}", identity + ): + raise ValueError("Every notebook cell needs a valid stable ID") + if identity in identifiers: + raise ValueError(f"Duplicate cell ID: {identity}") + identifiers.add(identity) + source = cell.get("source") + if not isinstance(source, list) or not all( + isinstance(line, str) for line in source + ): + raise ValueError(f"Invalid source in cell {identity}") + if not isinstance(cell.get("metadata"), dict): + raise ValueError(f"Invalid metadata in cell {identity}") + if cell.get("cell_type") == "code": + if cell.get("execution_count") is not None or cell.get("outputs") != []: + raise ValueError(f"Saved code outputs must be cleared: {identity}") + compile("".join(source), f"{path.name}:{identity}", "exec") + code_cells.append(cell) + elif cell.get("cell_type") != "markdown": + raise ValueError(f"Unexpected cell type: {identity}") + installation = [ + cell for cell in code_cells if INSTALL_TAG in cell["metadata"].get("tags", []) + ] + if len(installation) != 1 or installation[0] is not code_cells[0]: + raise ValueError("Exactly the first code cell must carry the installation tag") + return code_cells + + +def check(path: Path, *, run_install: bool = False) -> dict: + if sys.version_info < (3, 11): + raise ValueError("Python 3.11+ is required") + cells = read_notebook(path) + if run_install: + if ( + sys.prefix == sys.base_prefix + or importlib.util.find_spec("pitbridge") is not None + ): + raise ValueError( + "Use --run-install only in a fresh virtual environment " + "without PITBridge" + ) + namespace = {"__name__": "__main__"} + executed = 0 + for cell in cells: + if INSTALL_TAG in cell["metadata"].get("tags", []) and not run_install: + print( + "Skipping pinned installation cell; " + "checking the current interpreter's package." + ) + continue + exec( + compile("".join(cell["source"]), f"{path.name}:{cell['id']}", "exec"), + namespace, + ) + executed += 1 + if run_install: + origin = Path(namespace["pitbridge"].__file__).resolve() + if not origin.is_relative_to(Path(sys.prefix).resolve()): + raise ValueError( + f"PITBridge was imported from outside the new environment: {origin}" + ) + return { + "status": "verified", + "code_cells_executed": executed, + "installation_executed": run_install, + } + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + "--notebook", type=Path, default=ROOT / "examples/decision_time.ipynb" + ) + parser.add_argument( + "--run-install", + action="store_true", + help="Execute the pinned install cell, only in a fresh virtual environment", + ) + args = parser.parse_args() + print(json.dumps(check(args.notebook, run_install=args.run_install), indent=2))