Visualize payment time, space, and lifecycle behavior#

Goal. Explore a bounded, deterministic payment world through computed tables and plots.

Audience. Data scientists and ML engineers learning FraudTwin’s core simulation workflows.

Prerequisites. Python 3.12+, FraudTwin, and Polars. Optional plotting cells can install notebook packages.

Source size. 1,000 logical payments; outputs are reproducible and temporary.

Offline path. Analysis runs without Kafka, databases, or cloud services.

Optional notebook packages#

Run this only for richer plots or t-SNE output.

!pip install matplotlib scikit-learn

1. Generate the source world#

from pathlib import Path

import polars as pl

import fraudtwin
from fraudtwin.config import SimulationRunConfig, load_config

root = next(
    path
    for path in (Path.cwd(), *Path.cwd().parents)
    if (path / "configs" / "minimal.yaml").is_file()
)
base = load_config(root / "configs" / "minimal.yaml")
values = base.model_dump(mode="python")
values["payments"]["daily_target"] = 100
values["simulation"]["duration_days"] = 10
values["population"].update(
    {
        "customers": 100,
        "accounts": 200,
        "cards": 200,
        "merchants": 30,
        "devices": 200,
        "pix_keys": 100,
    }
)
values["behavior"]["amount_max"] = 100
values["simulation"]["seed"] = 2501
config = SimulationRunConfig.model_validate(values)
data = fraudtwin.generate(config)
print({"run_id": data.run_id, "payments": len(data.behavior.payments)})
assert len(data.behavior.payments) >= 1_000
{'run_id': 'RUN-f61020a6db72d42d', 'payments': 1000}

2. Analysis step#

payments = pl.DataFrame(
    [
        {
            "payment_id": p.payment_id,
            "rail": p.payment_rail,
            "amount": p.amount,
            "status": p.current_status,
        }
        for p in data.behavior.payments
    ]
)
display(payments.head())
assert payments.height == 1_000
shape: (5, 4)
payment_idrailamountstatus
strstrf64str
"PAY-00000001""ACCOUNT_TRANSFER"99.14"COMPLETED"
"PAY-00000002""CARD"60.55"SETTLED"
"PAY-00000003""ACCOUNT_TRANSFER"51.47"COMPLETED"
"PAY-00000004""ACCOUNT_TRANSFER"1.92"COMPLETED"
"PAY-00000005""PIX"10.19"RECEIVED"

3. Analysis step#

rail_counts = payments.group_by("rail").len().sort("len", descending=True)
rail_counts
shape: (3, 2)
raillen
stru32
"CARD"587
"PIX"228
"ACCOUNT_TRANSFER"185

4. Analysis step#

amount_summary = payments.select(
    [
        pl.col("amount").min().alias("min"),
        pl.col("amount").median().alias("median"),
        pl.col("amount").max().alias("max"),
    ]
)
amount_summary
shape: (1, 3)
minmedianmax
f64f64f64
1.2428.54100.0

5. Analysis step#

event_frame = pl.DataFrame(
    [
        {"event_type": e.event_type, "event_time": e.event_time, "payment_id": e.payment_id}
        for e in data.behavior.payment_events
    ]
)
event_frame.group_by("event_type").len().sort("len", descending=True).head(10)
shape: (10, 2)
event_typelen
stru32
"CARD_PAYMENT_INITIATED"587
"CARD_AUTHORIZATION_REQUESTED"587
"CARD_AUTHORIZED"532
"CARD_CAPTURED"519
"CARD_SETTLED"508
"CARD_CLEARED"508
"PIX_VALIDATED"228
"PIX_INITIATED"228
"PIX_SETTLED"219
"PIX_RECEIVED"219

6. Analysis step#

hourly = (
    event_frame.with_columns(pl.col("event_time").dt.hour().alias("hour"))
    .group_by("hour")
    .len()
    .sort("hour")
)
display(hourly)
shape: (24, 2)
hourlen
i8u32
058
163
271
3108
481
……
19199
20275
21274
22350
2349

7. Analysis step#

lifecycle = event_frame.group_by(["payment_id", "event_type"]).len()
print({"payments_with_events": lifecycle.select("payment_id").n_unique()})
{'payments_with_events': 1000}

8. Analysis step#

try:
    import matplotlib.pyplot as plt

    fig, axis = plt.subplots(figsize=(8, 4))
    axis.bar(hourly["hour"].to_list(), hourly["len"].to_list(), color="#0b7285")
    axis.set(title="Payment lifecycle events by hour", xlabel="UTC hour", ylabel="events")
    fig.tight_layout()
    plt.show()
    plt.close(fig)
except ImportError:
    print("Install matplotlib to render the lifecycle chart.")
../_images/55edb8abcf4647bbcde68a8cec9e467bf10691fea403d644851959dec034f8df.png

9. Analysis step#

fingerprint = fraudtwin.aggregate_fingerprint([dict(row) for row in payments.to_dicts()])
print({"payment_fingerprint": f"{fingerprint}"})
assert isinstance(fingerprint, str) and fingerprint
{'payment_fingerprint': 'cbbbc87581d82883ee5a4a957f7c037eff24f8fe08607e1295db754d788036f2'}
assert hourly.height > 0
assert fingerprint
print("Lifecycle analysis contract passed")