Audit quality faults and build observable projections#

Goal. Work through a bounded, reproducible example and inspect the evidence before connecting an external service.

Prerequisites. Base FraudTwin install. Optional extras and Docker commands are clearly marked.

Produces. Tables, fingerprints, manifests, and verification output.

Source size. The default cells generate approximately 1,000 logical payments; increase duration and population together for a 10,000-payment run.

Offline path. All marked offline cells run without Docker or network services. Service cells are optional and explicitly marked in notebook metadata.

Cleanup. Outputs are written under a temporary directory; remove any local run directory if you changed the output location.

!pip install pyiceberg[s3fs]
!pip install prometheus-client requests
Requirement already satisfied: pyiceberg[s3fs] in /home/emc/Projects/test/.venv/lib/python3.14/site-packages (0.12.0)
Requirement already satisfied: mmh3>=4.0.0 in /home/emc/Projects/test/.venv/lib/python3.14/site-packages (from pyiceberg[s3fs]) (5.3.0)
Requirement already satisfied: requests>=2.20.0 in /home/emc/Projects/test/.venv/lib/python3.14/site-packages (from pyiceberg[s3fs]) (2.34.2)
Requirement already satisfied: click>=7.1.1 in /home/emc/Projects/test/.venv/lib/python3.14/site-packages (from pyiceberg[s3fs]) (8.5.0)
Requirement already satisfied: rich>=10.11.0 in /home/emc/Projects/test/.venv/lib/python3.14/site-packages (from pyiceberg[s3fs]) (15.0.0)
Requirement already satisfied: strictyaml>=1.7.0 in /home/emc/Projects/test/.venv/lib/python3.14/site-packages (from pyiceberg[s3fs]) (1.7.3)
Requirement already satisfied: pydantic!=2.12.0,!=2.12.1,!=2.4.0,!=2.4.1,<3.0,>=2.0 in /home/emc/Projects/test/.venv/lib/python3.14/site-packages (from pyiceberg[s3fs]) (2.13.5)
Requirement already satisfied: fsspec>=2023.1.0 in /home/emc/Projects/test/.venv/lib/python3.14/site-packages (from pyiceberg[s3fs]) (2026.9.0)
Requirement already satisfied: pyparsing>=3.1.0 in /home/emc/Projects/test/.venv/lib/python3.14/site-packages (from pyiceberg[s3fs]) (3.3.3)
Requirement already satisfied: tenacity>=8.2.3 in /home/emc/Projects/test/.venv/lib/python3.14/site-packages (from pyiceberg[s3fs]) (9.1.4)
Requirement already satisfied: pyroaring>=1.0.0 in /home/emc/Projects/test/.venv/lib/python3.14/site-packages (from pyiceberg[s3fs]) (1.1.0)
Requirement already satisfied: cachetools>=5.5 in /home/emc/Projects/test/.venv/lib/python3.14/site-packages (from pyiceberg[s3fs]) (7.2.0)
Requirement already satisfied: zstandard>=0.13.0 in /home/emc/Projects/test/.venv/lib/python3.14/site-packages (from pyiceberg[s3fs]) (0.25.0)
Requirement already satisfied: s3fs>=2023.1.0 in /home/emc/Projects/test/.venv/lib/python3.14/site-packages (from pyiceberg[s3fs]) (2026.9.0)
Requirement already satisfied: annotated-types>=0.6.0 in /home/emc/Projects/test/.venv/lib/python3.14/site-packages (from pydantic!=2.12.0,!=2.12.1,!=2.4.0,!=2.4.1,<3.0,>=2.0->pyiceberg[s3fs]) (0.8.0)
Requirement already satisfied: pydantic-core==2.46.5 in /home/emc/Projects/test/.venv/lib/python3.14/site-packages (from pydantic!=2.12.0,!=2.12.1,!=2.4.0,!=2.4.1,<3.0,>=2.0->pyiceberg[s3fs]) (2.46.5)
Requirement already satisfied: typing-extensions>=4.14.1 in /home/emc/Projects/test/.venv/lib/python3.14/site-packages (from pydantic!=2.12.0,!=2.12.1,!=2.4.0,!=2.4.1,<3.0,>=2.0->pyiceberg[s3fs]) (4.16.0)
Requirement already satisfied: typing-inspection>=0.4.2 in /home/emc/Projects/test/.venv/lib/python3.14/site-packages (from pydantic!=2.12.0,!=2.12.1,!=2.4.0,!=2.4.1,<3.0,>=2.0->pyiceberg[s3fs]) (0.4.4)
Requirement already satisfied: charset_normalizer<4,>=2 in /home/emc/Projects/test/.venv/lib/python3.14/site-packages (from requests>=2.20.0->pyiceberg[s3fs]) (3.5.1)
Requirement already satisfied: idna<4,>=2.5 in /home/emc/Projects/test/.venv/lib/python3.14/site-packages (from requests>=2.20.0->pyiceberg[s3fs]) (3.19)
Requirement already satisfied: urllib3<3,>=1.26 in /home/emc/Projects/test/.venv/lib/python3.14/site-packages (from requests>=2.20.0->pyiceberg[s3fs]) (2.7.0)
Requirement already satisfied: certifi>=2023.5.7 in /home/emc/Projects/test/.venv/lib/python3.14/site-packages (from requests>=2.20.0->pyiceberg[s3fs]) (2026.7.22)
Requirement already satisfied: markdown-it-py>=2.2.0 in /home/emc/Projects/test/.venv/lib/python3.14/site-packages (from rich>=10.11.0->pyiceberg[s3fs]) (3.0.0)
Requirement already satisfied: pygments<3.0.0,>=2.13.0 in /home/emc/Projects/test/.venv/lib/python3.14/site-packages (from rich>=10.11.0->pyiceberg[s3fs]) (2.21.0)
Requirement already satisfied: mdurl~=0.1 in /home/emc/Projects/test/.venv/lib/python3.14/site-packages (from markdown-it-py>=2.2.0->rich>=10.11.0->pyiceberg[s3fs]) (0.1.2)
Requirement already satisfied: aiobotocore<4.0.0,>=2.19.0 in /home/emc/Projects/test/.venv/lib/python3.14/site-packages (from s3fs>=2023.1.0->pyiceberg[s3fs]) (3.9.1)
Requirement already satisfied: aiohttp!=4.0.0a0,!=4.0.0a1,>=3.9.0 in /home/emc/Projects/test/.venv/lib/python3.14/site-packages (from s3fs>=2023.1.0->pyiceberg[s3fs]) (3.14.3)
Requirement already satisfied: aioitertools<1.0.0,>=0.5.1 in /home/emc/Projects/test/.venv/lib/python3.14/site-packages (from aiobotocore<4.0.0,>=2.19.0->s3fs>=2023.1.0->pyiceberg[s3fs]) (0.13.0)
Requirement already satisfied: botocore<1.43.76,>=1.43.66 in /home/emc/Projects/test/.venv/lib/python3.14/site-packages (from aiobotocore<4.0.0,>=2.19.0->s3fs>=2023.1.0->pyiceberg[s3fs]) (1.43.75)
Requirement already satisfied: jmespath<2.0.0,>=0.7.1 in /home/emc/Projects/test/.venv/lib/python3.14/site-packages (from aiobotocore<4.0.0,>=2.19.0->s3fs>=2023.1.0->pyiceberg[s3fs]) (1.1.0)
Requirement already satisfied: multidict<7.0.0,>=6.0.0 in /home/emc/Projects/test/.venv/lib/python3.14/site-packages (from aiobotocore<4.0.0,>=2.19.0->s3fs>=2023.1.0->pyiceberg[s3fs]) (6.9.1)
Requirement already satisfied: python-dateutil<3.0.0,>=2.1 in /home/emc/Projects/test/.venv/lib/python3.14/site-packages (from aiobotocore<4.0.0,>=2.19.0->s3fs>=2023.1.0->pyiceberg[s3fs]) (2.9.0.post0)
Requirement already satisfied: wrapt<3.0.0,>=1.10.10 in /home/emc/Projects/test/.venv/lib/python3.14/site-packages (from aiobotocore<4.0.0,>=2.19.0->s3fs>=2023.1.0->pyiceberg[s3fs]) (2.4.1)
Requirement already satisfied: aiohappyeyeballs>=2.5.0 in /home/emc/Projects/test/.venv/lib/python3.14/site-packages (from aiohttp!=4.0.0a0,!=4.0.0a1,>=3.9.0->s3fs>=2023.1.0->pyiceberg[s3fs]) (2.7.1)
Requirement already satisfied: aiosignal>=1.4.0 in /home/emc/Projects/test/.venv/lib/python3.14/site-packages (from aiohttp!=4.0.0a0,!=4.0.0a1,>=3.9.0->s3fs>=2023.1.0->pyiceberg[s3fs]) (1.4.0)
Requirement already satisfied: attrs>=17.3.0 in /home/emc/Projects/test/.venv/lib/python3.14/site-packages (from aiohttp!=4.0.0a0,!=4.0.0a1,>=3.9.0->s3fs>=2023.1.0->pyiceberg[s3fs]) (26.1.0)
Requirement already satisfied: frozenlist>=1.1.1 in /home/emc/Projects/test/.venv/lib/python3.14/site-packages (from aiohttp!=4.0.0a0,!=4.0.0a1,>=3.9.0->s3fs>=2023.1.0->pyiceberg[s3fs]) (1.8.0)
Requirement already satisfied: propcache>=0.2.0 in /home/emc/Projects/test/.venv/lib/python3.14/site-packages (from aiohttp!=4.0.0a0,!=4.0.0a1,>=3.9.0->s3fs>=2023.1.0->pyiceberg[s3fs]) (0.5.4)
Requirement already satisfied: yarl<2.0,>=1.17.0 in /home/emc/Projects/test/.venv/lib/python3.14/site-packages (from aiohttp!=4.0.0a0,!=4.0.0a1,>=3.9.0->s3fs>=2023.1.0->pyiceberg[s3fs]) (1.25.1)
Requirement already satisfied: six>=1.5 in /home/emc/Projects/test/.venv/lib/python3.14/site-packages (from python-dateutil<3.0.0,>=2.1->aiobotocore<4.0.0,>=2.19.0->s3fs>=2023.1.0->pyiceberg[s3fs]) (1.17.0)
Requirement already satisfied: prometheus-client in /home/emc/Projects/test/.venv/lib/python3.14/site-packages (0.26.0)
Requirement already satisfied: requests in /home/emc/Projects/test/.venv/lib/python3.14/site-packages (2.34.2)
Requirement already satisfied: charset_normalizer<4,>=2 in /home/emc/Projects/test/.venv/lib/python3.14/site-packages (from requests) (3.5.1)
Requirement already satisfied: idna<4,>=2.5 in /home/emc/Projects/test/.venv/lib/python3.14/site-packages (from requests) (3.19)
Requirement already satisfied: urllib3<3,>=1.26 in /home/emc/Projects/test/.venv/lib/python3.14/site-packages (from requests) (2.7.0)
Requirement already satisfied: certifi>=2023.5.7 in /home/emc/Projects/test/.venv/lib/python3.14/site-packages (from requests) (2026.7.22)

Optional lakehouse and observability setup#

The first cell installs the clients. Start the local catalog and metrics stack only for service-backed verification; operational projections and SLO calculations remain offline.

docker compose --profile lakehouse --profile observability up -d minio minio-init iceberg-rest prometheus grafana

Stop with docker compose --profile lakehouse --profile observability down; unavailable services use the local fallback.

Set up a deterministic source run

import json
from pathlib import Path

import polars as pl

from fraudtwin.config import load_config
from fraudtwin.generation import generate

root = next(
    (p for p in (Path.cwd(), *Path.cwd().parents) if (p / "configs" / "minimal.yaml").exists()),
    Path.cwd(),
)
base = load_config(root / "configs" / "minimal.yaml")
# Scale the population so the bounded example produces about 1,000 payments.
population = base.population.model_copy(
    update={
        "customers": 200,
        "accounts": 300,
        "cards": 240,
        "devices": 240,
        "pix_keys": 160,
        "merchants": 60,
    }
)
simulation = base.simulation.model_copy(update={"duration_days": 10})
fraud = base.fraud.model_copy(update={"enabled": True, "target_rate": 0.05})
config = base.model_copy(
    update={"population": population, "simulation": simulation, "fraud": fraud}
)
data = generate(config, write=False)
run_id = data.run_id
payments = pl.DataFrame([item.model_dump(mode="json") for item in data.behavior.payments])
print({"run_id": run_id, "payments": len(payments), "events": len(data.behavior.payment_events)})
{'run_id': 'RUN-2a3ad02ee370aeb8', 'payments': 1092, 'events': 4699}

Inspect schema, grain, and counts

from fraudtwin.lakehouse import build_bronze_records, silver_rows

bronze = build_bronze_records(data.entities, data.behavior, data.manifest, include_oracle=False)
print({"bronze": len(bronze), "subjects": sorted({r.subject for r in bronze})})
{'bronze': 9386, 'subjects': ['accounts', 'behavior_profiles', 'cards', 'customer-dispute', 'customers', 'devices', 'fraud-alert', 'fraud-case', 'fraud-case-confirmation', 'fraud-label', 'institutions', 'ledger_entries', 'merchants', 'payment-event', 'payments', 'pix_keys']}

Run the core operation

display(pl.DataFrame([r.as_row() for r in bronze[:10]]))
shape: (10, 16)
run_idsourcesubjecttable_nameevent_familyrecord_idrecord_keycontract_fingerprintpayload_jsonraw_payload_b64headers_jsonevent_timereceived_atkafka_topickafka_partitionkafka_offset
strstrstrstrstrstrstrstrstrstrstrstrstrnullnullnull
"RUN-2a3ad02ee370aeb8""parquet""payment-event""payment_events""payment-event""EVT-F01-000001-000001""PAY-F01-000001-000001""e7f8a63404fb5037bd6e83333550eb…"{"account_id":"ACC-000123","af…"KkVWVC1GMDEtMDAwMDAxLTAwMDAwMS…"{"fraudtwin-contract-fingerpri…"2026-01-01T00:00:00+00:00""2026-01-01T00:00:00+00:00"nullnullnull
"RUN-2a3ad02ee370aeb8""parquet""payment-event""payment_events""payment-event""EVT-F02-000002-000001""PAY-F02-000002-000001""e7f8a63404fb5037bd6e83333550eb…"{"account_id":"ACC-000142","af…"KkVWVC1GMDItMDAwMDAyLTAwMDAwMS…"{"fraudtwin-contract-fingerpri…"2026-01-01T00:00:00+00:00""2026-01-01T00:00:00+00:00"nullnullnull
"RUN-2a3ad02ee370aeb8""parquet""payment-event""payment_events""payment-event""EVT-F03-000003-SIGNAL-01""PAY-F03-000003-000001""e7f8a63404fb5037bd6e83333550eb…"{"account_id":"ACC-000021","af…"MEVWVC1GMDMtMDAwMDAzLVNJR05BTC…"{"fraudtwin-contract-fingerpri…"2026-01-01T00:00:00+00:00""2026-01-01T00:00:00+00:00"nullnullnull
"RUN-2a3ad02ee370aeb8""parquet""payment-event""payment_events""payment-event""EVT-F04-000004-000001""PAY-F04-000004-000001""e7f8a63404fb5037bd6e83333550eb…"{"account_id":"ACC-000021","af…"KkVWVC1GMDQtMDAwMDA0LTAwMDAwMR…"{"fraudtwin-contract-fingerpri…"2026-01-01T00:00:00+00:00""2026-01-01T00:00:00+00:00"nullnullnull
"RUN-2a3ad02ee370aeb8""parquet""payment-event""payment_events""payment-event""EVT-F05-000005-000001""PAY-F05-000005-000001""e7f8a63404fb5037bd6e83333550eb…"{"account_id":"ACC-000023","af…"KkVWVC1GMDUtMDAwMDA1LTAwMDAwMS…"{"fraudtwin-contract-fingerpri…"2026-01-01T00:00:00+00:00""2026-01-01T00:00:00+00:00"nullnullnull
"RUN-2a3ad02ee370aeb8""parquet""payment-event""payment_events""payment-event""EVT-HN-F01-000001-000001""PAY-HN-F01-000001-000001""e7f8a63404fb5037bd6e83333550eb…"{"account_id":"ACC-000186","af…"MEVWVC1ITi1GMDEtMDAwMDAxLTAwMD…"{"fraudtwin-contract-fingerpri…"2026-01-01T00:00:00+00:00""2026-01-01T00:00:00+00:00"nullnullnull
"RUN-2a3ad02ee370aeb8""parquet""payment-event""payment_events""payment-event""EVT-HN-F02-000002-000001""PAY-HN-F02-000002-000001""e7f8a63404fb5037bd6e83333550eb…"{"account_id":"ACC-000294","af…"MEVWVC1ITi1GMDItMDAwMDAyLTAwMD…"{"fraudtwin-contract-fingerpri…"2026-01-01T00:00:00+00:00""2026-01-01T00:00:00+00:00"nullnullnull
"RUN-2a3ad02ee370aeb8""parquet""payment-event""payment_events""payment-event""EVT-HN-F03-000003-SIGNAL-01""PAY-HN-F03-000003-000001""e7f8a63404fb5037bd6e83333550eb…"{"account_id":"ACC-000021","af…"NkVWVC1ITi1GMDMtMDAwMDAzLVNJR0…"{"fraudtwin-contract-fingerpri…"2026-01-01T00:00:00+00:00""2026-01-01T00:00:00+00:00"nullnullnull
"RUN-2a3ad02ee370aeb8""parquet""payment-event""payment_events""payment-event""EVT-HN-F04-000004-000001""PAY-HN-F04-000004-000001""e7f8a63404fb5037bd6e83333550eb…"{"account_id":"ACC-000021","af…"MEVWVC1ITi1GMDQtMDAwMDA0LTAwMD…"{"fraudtwin-contract-fingerpri…"2026-01-01T00:00:00+00:00""2026-01-01T00:00:00+00:00"nullnullnull
"RUN-2a3ad02ee370aeb8""parquet""payment-event""payment_events""payment-event""EVT-HN-F05-000005-000001""PAY-HN-F05-000005-000001""e7f8a63404fb5037bd6e83333550eb…"{"account_id":"ACC-000144","af…"MEVWVC1ITi1GMDUtMDAwMDA1LTAwMD…"{"fraudtwin-contract-fingerpri…"2026-01-01T00:00:00+00:00""2026-01-01T00:00:00+00:00"nullnullnull

Measure and interpret the result

silver = silver_rows(bronze)
print({"silver": len(silver), "duplicates_removed": len(bronze) - len(silver)})
{'silver': 9386, 'duplicates_removed': 0}

Exercise a parameter or failure mode

quality = {
    "null_payloads": sum(not row["payload_json"] for row in silver),
    "unique_ids": len({row["record_id"] for row in silver}),
}
print(quality)
{'null_payloads': 0, 'unique_ids': 9386}

Write a compact artifact and fingerprint

assert quality["null_payloads"] == 0
print("observable projection verified; oracle tables remain excluded")
observable projection verified; oracle tables remain excluded

Verify invariants and clean up

# A compact inspection is more useful than printing an entire run.
sample_columns = [
    c for c in ("payment_id", "amount", "initiated_at", "payer_account_id") if c in payments.columns
]
sample_rows = payments.select(sample_columns).head(8).to_dicts()
print(f"Sample payments ({len(sample_rows)} of {payments.height} rows):")
for row in sample_rows:
    print(
        f"  - {row.get('payment_id')}: amount={row.get('amount')}, "
        f"initiated_at={row.get('initiated_at')}, payer={row.get('payer_account_id')}"
    )
nulls = {name: count for name, count in payments.null_count().to_dicts()[0].items() if count}
print("\nData quality summary:")
print(f"  rows: {payments.height}")
print(f"  columns: {payments.width}")
if not nulls:
    print("  nulls: none")
else:
    print("  columns with nulls:")
    for name, count in sorted(nulls.items()):
        print(f"    - {name}: {count}")
Sample payments (8 of 1092 rows):
  - PAY-00000001: amount=70.7, initiated_at=2026-01-03T16:25:00Z, payer=ACC-000123
  - PAY-00000002: amount=25.52, initiated_at=2026-01-05T11:37:00Z, payer=ACC-000174
  - PAY-00000003: amount=18.37, initiated_at=2026-01-05T11:21:00Z, payer=ACC-000174
  - PAY-00000004: amount=5.54, initiated_at=2026-01-02T22:30:00Z, payer=ACC-000003
  - PAY-00000005: amount=13.71, initiated_at=2026-01-02T09:41:00Z, payer=ACC-000029
  - PAY-00000006: amount=70.06, initiated_at=2026-01-04T10:40:00Z, payer=ACC-000179
  - PAY-00000007: amount=42.73, initiated_at=2026-01-02T18:04:00Z, payer=ACC-000247
  - PAY-00000008: amount=36.42, initiated_at=2026-01-05T09:27:00Z, payer=ACC-000255

Data quality summary:
  rows: 1092
  columns: 15
  columns with nulls:
    - card_id: 565
    - merchant_id: 565
    - payee_account_id: 86
    - payee_institution_id: 86
    - payee_pix_key_id: 857
    - payer_institution_id: 86
    - payer_pix_key_id: 857

Optional service integration

summary = {
    "run_id": run_id,
    "payments": len(data.behavior.payments),
    "payment_events": len(data.behavior.payment_events),
    "fraud_records": len(data.behavior.fraud_records),
}
assert summary["payments"] == len(payments)
assert summary["payments"] > 0
print(json.dumps(summary, indent=2, default=str))
{
  "run_id": "RUN-2a3ad02ee370aeb8",
  "payments": 1092,
  "payment_events": 4699,
  "fraud_records": 51
}

Review the expected outcome

import os

try:
    from pyiceberg.catalog import load_catalog

    catalog = load_catalog(
        "fraudtwin",
        type="rest",
        uri=os.getenv("FRAUDTWIN_ICEBERG_CATALOG_URI", "http://localhost:8181"),
    )
    import requests

    namespaces = catalog.list_namespaces()
    prometheus = requests.get("http://localhost:9090/-/ready", timeout=3)
    print({"connected": True, "namespaces": namespaces, "prometheus": prometheus.status_code})
except Exception as exc:
    print({"connected": False, "offline_fallback": True, "reason": type(exc).__name__})
{'connected': True, 'namespaces': [], 'prometheus': 200}

Record the generated shape and tutorial contract.#

summary = {
    "payments": len(data.behavior.payments),
    "events": len(data.behavior.payment_events),
}
print(summary)
assert summary["payments"] >= 0
{'payments': 1092, 'events': 4699}
assert len(bronze) > 0
print({"offline_fallback": True, "bronze_records": len(bronze)})
{'offline_fallback': True, 'bronze_records': 9386}