Skip to content

Tracking a run

The decorator

@track is the whole integration: one decorator, one Source describing where the training data comes from. ConsentML loads the data itself, so what it records can't disagree with what the model actually trained on:

from consentml import track
from consentml.sources.postgres import PostgresSource

@track(
    model_name="readmission-risk",
    source=PostgresSource(
        dsn="postgresql://user:pw@db.internal/clinic",
        query="""
            SELECT p.patient_id, p.age, l.ldl, p.outcome
            FROM patients p JOIN labs l USING (patient_id)
        """,
        subject_id_col="patient_id",
    ),
)
def train(df):
    return LogisticRegression().fit(df[["age", "ldl"]], df["outcome"])

model = train()    # no argument: ConsentML supplies the data

PostgresSource needs the consentml[postgres] extra. The query runs in a read-only transaction — ConsentML never writes to the database it reads lineage from — and only host, port and database name are recorded; credentials in the DSN never reach the stored provenance. See the Postgres guide for the connection and query requirements.

Sources

source= is evaluated once, when the decorator runs, not each time the wrapped function is called. That means @track binds a function to one source for its lifetime: it cannot decorate a function meant to be called later against different data, because the source — and therefore the data that will be loaded — is already fixed by the time train() is called for the first time. A training pipeline that trains the same function against several datasets needs a separate @track-decorated function, or a separate call site, per source.

DataFrameSource

For data already in memory, DataFrameSource reads subject IDs out of a column you name:

from consentml.sources import DataFrameSource

@track(model_name="m", source=DataFrameSource(df, subject_id_col="patient_id",
                                              label="clinic.patients"))
def train(df): ...

label is caller-asserted: ConsentML has no way to check where an in-memory frame actually came from, unlike PostgresSource, which ran the query itself. The stored provenance records the label under kind="dataframe" precisely so a reader of the audit trail can tell a self-reported label apart from a connector-verified one — it is not a claim ConsentML is vouching for.

What gets recorded

A PostgresSource run is recorded with the exact query text and its SHA-256, plus the tables the query plan touched:

{
    "kind": "postgres",
    "host": "db.internal",
    "port": 5432,
    "database": "clinic",
    "query": "SELECT p.patient_id, p.age, l.ldl, p.outcome FROM patients p JOIN labs l USING (patient_id)",
    "query_sha256": "ac80473d33489424...",
    "referenced_tables": ["public.labs", "public.patients"],
    "referenced_tables_source": "explain",
    "n_rows": 3
}

The query text is authoritative; referenced_tables is advisory — it comes from EXPLAIN, so a table the planner optimizes away won't appear there. referenced_tables_source says which mechanism produced the list, or "unavailable" if EXPLAIN couldn't run. A DataFrameSource run's provenance is smaller — kind, the asserted label, the subject ID column, and the row count — since there is no query to record.

Subject IDs themselves are never stored raw. Both sources hash each subject ID to its SHA-256 digest before it reaches the database, so the lineage store never holds a raw patient ID, email address, or other identifier — only a digest that a later revoke call can re-derive from the same input and match against.

Null subject IDs are refused

Both sources reject a null in the subject ID column outright. Given a frame with one missing patient_id, @track raises before training runs:

ConsentMLError: Subject ID column 'patient_id' has 1 null value(s); a null subject ID cannot be revoked, so refusing to record it as training coverage.

A null identifies no one in particular, so it can never be matched by a future revocation. Recording it anyway would inflate the run's subject count with a subject no revocation could ever reach — a training run that looks like it covers one more person than any revoke call could actually find.