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.
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[...]) and direct mutation aren't covered — see
Limitations for the honest list.
Installation
Python 3.11+, pandas 2.2+. Fully typed.
pip install pandatrace
Usage
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.report() renders the whole history as a plain-text table — one line
per step, with row deltas, column churn, and the nulls each step introduced —
and in a notebook a bare ledger at the end of a cell renders the same table
as HTML. Ledger.to_json() exports it (plain ints only, nothing
numpy-shaped) for logging or diffing between runs:
print(result.ledger.report())
result.ledger.to_json(indent=2)
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_OPSinpandatrace.tracedcoverspipe,query,assign,rename,drop,dropna,fillna,drop_duplicates,sort_values,sort_index,reindex,reset_index,head,tail,sample,merge, andjoin, plus apandatrace.concatshim for the module-levelpd.concat. Anything else that returns a new frame (groupby,apply,pivot, …) behaves like normal pandas but records nothing — the ledger'sSnapshot/Stepwon'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"] = 1bypasses every hook; there's no__setitem__interception. inplace=Trueis accepted but silently untracked, not rejected. This is deliberate, not an oversight: pandas' own internals rely on calling tracked methods withinplace=Trueas plumbing (merge()strips duplicate join-key columns via an internaldrop(inplace=True)), so rejecting it outright breaksmerge/joinfrom the inside. A step needs abeforesnapshot 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/concatacross 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'sdetailso 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 staledepswill quietly misreport provenance. - Null counts are addressed by name, and duplicate labels defeat that.
concat(axis=1), a collidingrename, and labels that differ to pandas but collide understr()all map several real columns onto one key. Counts are folded together rather than one silently overwriting another, andSnapshot.duplicated_columnstells 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.
Development
Managed with uv: uv sync, then
scripts/test.sh # pytest, args passed through
scripts/coverage.sh # suite under coverage (gate: 90%)
scripts/lint.sh # ruff check + ty check (both clean)
scripts/docs.sh # sphinx with -W; also executes the example notebook
scripts/check.sh # all of the above, the pre-merge gate
Docs build clean and are built with -W so warnings fail — keep it that way.
CI enforces the full test matrix (Python 3.11–3.14 × pandas 2.2/2.3/3.x) plus
all of the gates above on every push.
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
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file pandatrace-1.1.0.tar.gz.
File metadata
- Download URL: pandatrace-1.1.0.tar.gz
- Upload date:
- Size: 16.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ade2b43099b761cfabea9d2163455da4420ae2cef4cd64f5b295a52254f04728
|
|
| MD5 |
f8e79b1e4cb51c3faaf8bf76354d218f
|
|
| BLAKE2b-256 |
d57b73194fd8e316cc1238fc6a6994803d0e16f683e241d1b54cf6a093a30537
|
Provenance
The following attestation bundles were made for pandatrace-1.1.0.tar.gz:
Publisher:
release.yml on Pikaryu729/pandatrace
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pandatrace-1.1.0.tar.gz -
Subject digest:
ade2b43099b761cfabea9d2163455da4420ae2cef4cd64f5b295a52254f04728 - Sigstore transparency entry: 2410285623
- Sigstore integration time:
-
Permalink:
Pikaryu729/pandatrace@900c0462bcbc49f8e838727829d63ca5900c77e3 -
Branch / Tag:
refs/tags/v1.1.0 - Owner: https://github.com/Pikaryu729
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@900c0462bcbc49f8e838727829d63ca5900c77e3 -
Trigger Event:
release
-
Statement type:
File details
Details for the file pandatrace-1.1.0-py3-none-any.whl.
File metadata
- Download URL: pandatrace-1.1.0-py3-none-any.whl
- Upload date:
- Size: 18.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e29ec6efcdbdb84553e91d9ea1d23d5a1cb35c6969600f20822c418eeea6877a
|
|
| MD5 |
82d0a3f98bbbe81ba659e695fbdb3529
|
|
| BLAKE2b-256 |
6f623cd371535573fbe820885956b56df371a14eb3c8f296cae499f517cc0fc9
|
Provenance
The following attestation bundles were made for pandatrace-1.1.0-py3-none-any.whl:
Publisher:
release.yml on Pikaryu729/pandatrace
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pandatrace-1.1.0-py3-none-any.whl -
Subject digest:
e29ec6efcdbdb84553e91d9ea1d23d5a1cb35c6969600f20822c418eeea6877a - Sigstore transparency entry: 2410285681
- Sigstore integration time:
-
Permalink:
Pikaryu729/pandatrace@900c0462bcbc49f8e838727829d63ca5900c77e3 -
Branch / Tag:
refs/tags/v1.1.0 - Owner: https://github.com/Pikaryu729
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@900c0462bcbc49f8e838727829d63ca5900c77e3 -
Trigger Event:
release
-
Statement type: