Skip to main content

pandatrace

Provenance tracking for pandas pipelines — what each operation did to your frame, which columns it created, and where the nulls came from.

The goal is that after a long chain of transformations you can trace a frame back through its whole history, even across however many intermediate variables you bound along the way.

Status: early, but the core premise now works. TracedDataFrame automatically records a whitelisted set of operations — both df = df.sort_index() reassignment and df.pipe(f).sort_index()... chaining are tracked correctly into one shared ledger. Indexing (.loc, .iloc, df[...]), direct mutation, and a few other things aren't covered yet. See Limitations.

Requirements

Python 3.14+, pandas 3.x. Managed with uv.

uv sync

What works today

TracedDataFrame is a pd.DataFrame subclass — use it like a normal DataFrame, and a whitelisted set of operations get recorded automatically, whichever style you write them in:

import pandas as pd
from pandatrace import TracedDataFrame

orders = TracedDataFrame(
    {
        "id": [1, 2, 3, 4],
        "price": [10.0, 20.0, None, 40.0],
        "qty": [1, 2, 3, 4],
    },
    source="orders.csv",
)
customers = pd.DataFrame({"id": [1, 2, 3], "region": ["eu", "us", "eu"]})

result = orders.merge(customers, on="id", how="left").assign(
    total=lambda d: d["price"] * d["qty"]
)

for step in result.ledger.steps:
    print(
        f"{step.index}. {step.op:8} rows={step.row_delta} "
        f"new={step.new_columns} nulls={step.nulls_introduced()}"
    )
0. read     rows=None new=() nulls={}
1. merge    rows=0 new=('region',) nulls={'region': 1}
2. assign   rows=0 new=('total',) nulls={'total': 1}

The left join brought in a region that is null for the unmatched order, and the total inherited the null from price — both attributed to the step that introduced them, with no manual bookkeeping. This works identically whether you write it as one chain (above), reassign at every step (orders = orders.merge(...); orders = orders.assign(...)), or split the chain across .pipe() calls — the same Ledger is shared across every frame descended from the same original, however many intermediate variables you bind or leave unbound. You can then trace a column exactly as before:

result.ledger.parents_of("total")  # ('price', 'qty')
[s.op for s in result.ledger.origin_of("region")]  # ['merge']

Ledger and its supporting types are also usable directly, for anything outside the tracked whitelist:

from pandatrace import Ledger

ledger = Ledger(df, source="orders.csv")
ledger.record("custom_op", before_df, after_df, deps={"total": ("price", "qty")})

The layers

Type Role
Snapshot Frozen structural summary of a frame: row count, column labels, nulls per column. Holds no reference to the frame, so it stays cheap and cannot go stale when you mutate the original.
Step Frozen record of one operation, with before/after snapshots, optional column deps, and a call-site origin. Deltas are derived from the snapshots on demand, never stored.
Ledger The one mutable object. Append-only list of steps, seeded with a read step at index 0.
TracedDataFrame A pd.DataFrame subclass. Wraps a whitelisted set of methods (TRACKED_OPS in pandatrace.traced) to call Ledger.record() automatically; everything else behaves like a normal DataFrame.

Limitations

Worth knowing before you reach for this:

  • Only a whitelisted set of operations is tracked. TRACKED_OPS in pandatrace.traced covers pipe, query, assign, rename, drop, dropna, fillna, drop_duplicates, sort_values, sort_index, reindex, reset_index, head, tail, sample, merge, and join, plus a pandatrace.concat shim for the module-level pd.concat. Anything else that returns a new frame (groupby, apply, pivot, …) behaves like normal pandas but records nothing — the ledger's Snapshot/Step won't lie about what happened, but it will have a gap.
  • Indexing isn't tracked. .loc, .iloc, df[...], and boolean masking all fall outside the wrapped whitelist — column subsetting is a real provenance event this doesn't see yet.
  • Direct mutation is invisible. traced_df["x"] = 1 bypasses every hook; there's no __setitem__ interception.
  • inplace=True is accepted but silently untracked, not rejected. This is deliberate, not an oversight: pandas' own internals rely on calling tracked methods with inplace=True as plumbing (merge() strips duplicate join-key columns via an internal drop(inplace=True)), so rejecting it outright breaks merge/join from the inside. A step needs a before snapshot taken before the mutation happens, which an in-place call doesn't give a clean opportunity for — so it's recorded as nothing rather than guessed at.
  • merge/concat across two independently-ledgered frames keep only one history. The first/left operand's ledger survives; the other operand's step count is noted in the recorded step's detail so the join stays auditable, but its full history isn't merged in.
  • Dependencies are declared, not inferred. deps={"total": ("price", "qty")} is something you assert. Nothing verifies it against what the operation really did, so a stale deps will quietly misreport provenance.
  • Null counts are addressed by name, and duplicate labels defeat that. concat(axis=1), a colliding rename, and labels that differ to pandas but collide under str() all map several real columns onto one key. Counts are folded together rather than one silently overwriting another, and Snapshot.duplicated_columns tells you which labels that happened to — but the per-column figure is genuinely lost, not recoverable.
  • Snapshots are structural only. Row counts, column labels, and null counts. No dtypes, no index information, no value-level diffing.
  • ruff check and ty check are not currently clean.

Development

uv run pytest                              # 93 passing, 1 strict xfail (see above)
uv run coverage run -m pytest && uv run coverage report   # gate: 90%
uv run ruff check . [--fix]
uv run ty check
uv run sphinx-build -W -b html docs docs/_build/html

Docs build clean and are built with -W so warnings fail — keep it that way.

Longer prose docs, including the design invariants, live in docs/ (index.rst for the narrative, api.rst for the autodoc API reference).

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

pandatrace-1.0.0.tar.gz (13.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

pandatrace-1.0.0-py3-none-any.whl (14.8 kB view details)

Uploaded Python 3

File details

Details for the file pandatrace-1.0.0.tar.gz.

File metadata

  • Download URL: pandatrace-1.0.0.tar.gz
  • Upload date:
  • Size: 13.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for pandatrace-1.0.0.tar.gz
Algorithm Hash digest
SHA256 4fce91b1be4ec86845d29eed9fcaa257606b0647f849fdd9b71204be65a04a10
MD5 2e72815b0ddb3aacbaf0dcd18117db51
BLAKE2b-256 5aaeb41cd75f6703d1245fa61a6bc71c81edf0081123f24534a20492881c55e2

See more details on using hashes here.

Provenance

The following attestation bundles were made for pandatrace-1.0.0.tar.gz:

Publisher: release.yml on Pikaryu729/pandatrace

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file pandatrace-1.0.0-py3-none-any.whl.

File metadata

  • Download URL: pandatrace-1.0.0-py3-none-any.whl
  • Upload date:
  • Size: 14.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for pandatrace-1.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 7ca32af2436ca910f493ab3794b304ce1bb7f981d605415bb15d808c8c82b906
MD5 ef2ae0667c1990e5da93add6af909498
BLAKE2b-256 e618fa66ee79b848ff71debd85710563cc4947dd9274fb14c7ec6041018415cb

See more details on using hashes here.

Provenance

The following attestation bundles were made for pandatrace-1.0.0-py3-none-any.whl:

Publisher: release.yml on Pikaryu729/pandatrace

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

1.2.0

2 files

1.1.0.post1

2 files

1.1.0

2 files

1.0.0.post1

2 files

This release

1.0.0 This release

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page