Skip to main content
Pre-release

This release is a pre-release and may not be stable for production use.

terrine

Build datasets directly from event logs.

You have event tables: orders, sessions, payments. You want a modeling table, for example one row per customer per snapshot date, with features computed from the past and labels from the future. Terrine builds that table in your warehouse (DuckDB, PostgreSQL, BigQuery, Snowflake, and more) from a few lines of Python.

Why Terrine?

Most data science datasets come in a few shapes:

  • snapshots with features and labels;
  • cohort curves;
  • time-to-event tables;
  • before-and-after windows around an event.

Each time, the SQL is the same pile of date joins and window boundaries, rewritten by hand. Terrine takes the shape as a starting point. You name the rows you want and the windows to aggregate over; Terrine writes and runs the queries.

Because Terrine knows each row's snapshot time, features can only look backwards and targets only forwards. Each value also says whether its window was fully covered by your data, so you decide how to treat gaps for your problem.

Example: a customer spend forecasting table

Requires Python 3.13 or 3.14.

pip install --pre "terrine[duckdb,pandas]"
import pandas as pd
import terrine as tr

customers = pd.DataFrame({"customer_id": ["alice", "bob", "carol"]})
orders = pd.DataFrame({
    "customer_id": ["alice", "alice", "alice", "alice", "bob"],
    "ordered_at": pd.to_datetime(
        ["2025-01-03", "2025-01-20", "2025-02-10", "2025-03-05", "2025-01-05"]
    ),
    "amount": [30.0, 45.0, 25.0, 60.0, 80.0],
})

# Who the rows are about, which table holds the events, what to measure,
# and which calendar days (inclusive) the orders table is fully loaded for.
model = tr.Model.from_tables(
    units={"customer": tr.UnitSource(customers, "customer_id")},
    events={"orders": tr.Events(
        orders, time="ordered_at", key="customer_id",
        metrics={"orders": tr.count(), "spend": tr.sum("amount")},
        complete=("2025-01-01", "2025-03-20"),
    )},
)

# One row per customer on the first of each month, February through March:
# the last 30 days as features, spend in the next 30 days as the target.
design = tr.recipes.time_series(
    every="1mo", start="2025-02-01", end="2025-03-01",
    features={
        "orders_30d": tr.agg("orders", window=("-30d", "0s")),
        "spend_30d": tr.agg("spend", window=("-30d", "0s")),
    },
    outcomes={"spend_next_30d": tr.agg("spend", window=("0s", "30d"))},
)
print(design.describe(model))

data = design.build(model).to_pandas()
columns = ["unit", "t0", "orders_30d", "spend_30d", "spend_next_30d", "spend_next_30d__reason"]
print(data[columns].assign(t0=data["t0"].dt.date).sort_values(["t0", "unit"]).to_string(index=False))

describe explains the rows, windows and declared completeness without querying the source tables (sample summary). Running the example prints:

One row per customer (table customer, key customer_id) at each snapshot.
Snapshots: every 1 month from 2025-02-01 through 2025-03-01 (2 dates: 2025-02-01, 2025-03-01), at
  00:00 UTC.
Features (before t0):
  orders_30d: count of orders in [t0 - 30d, t0)
  spend_30d: sum of orders.amount in [t0 - 30d, t0)
Outcomes (after t0):
  spend_next_30d: sum of orders.amount in [t0, t0 + 30d)
Completeness: orders is declared complete for 2025-01-01 through 2025-03-20 (UTC days).
  A metric window outside declared source coverage is null with reason incomplete_coverage; unit
    observation bounds use incomplete_observation.
Empty complete metric windows (no matching events):
  orders_30d: 0 (no reason)
  spend_30d: 0 (no reason)
  spend_next_30d: 0 (no reason)
Columns: unit, anchor_id, t0, orders_30d, orders_30d__complete, orders_30d__reason, spend_30d,
  spend_30d__complete, spend_30d__reason, spend_next_30d, spend_next_30d__complete,
  spend_next_30d__reason.
 unit         t0  orders_30d  spend_30d  spend_next_30d spend_next_30d__reason
alice 2025-02-01           2       75.0            25.0                   <NA>
  bob 2025-02-01           1       80.0             0.0                   <NA>
carol 2025-02-01           0        0.0             0.0                   <NA>
alice 2025-03-01           1       25.0            <NA>    incomplete_coverage
  bob 2025-03-01           0        0.0            <NA>    incomplete_coverage
carol 2025-03-01           0        0.0            <NA>    incomplete_coverage

(Only some columns are printed; t0 is shown as a date.)

Reading the output:

  • Carol has no orders. She still has a row at each snapshot, with zero counts.
  • Alice's February 10 order is in her February 1 target and her March 1 features. The same order is the future for one row and the past for the next.
  • The March targets are unknown. Their 30-day window runs past March 20, where the orders data ends. Drop them, keep them for scoring, or handle them however suits your model.

You name the metrics: tr.count() counts events, tr.sum("amount") adds a column, and tr.count(where={"status": "refunded"}) counts only matching events once status is declared in labels=. Every value has a <name>__complete flag and a <name>__reason column, so you can filter or impute deliberately.

The four ideas

Idea In the example
Unit: who each row is about, read from a table so nobody silently drops out customers.customer_id
t0: the snapshot time of each row. every is an elapsed duration ("7d", "4w") or a calendar step ("1mo", "3mo", "1y"); a monthly schedule from the 31st lands on Jan 31, Feb 28, Mar 31 every="1mo": the first of each month from 2025-02-01 through 2025-03-01, both days included
Windows: relative to t0, start included, end excluded. Features must end at or before t0; outcomes must start at or after t0. Windows are elapsed time: "-1mo" is refused, use "-30d" or "-4w" ("-30d", "0s") is the 30 days before t0; ("0s", "30d") is the 30 days after
Completeness: the calendar days, inclusive, that you declare fully loaded for each event table (not just the first and last event you happen to have). Windows outside them are unknown, not zero complete=("2025-01-01", "2025-03-20")

Other datasets

tr.recipes covers the usual shapes. Each one returns a normal Design you can edit.

You want Recipe Guide
Forecasting, propensity and other features-plus-target tables time_series Scheduled time series
Customer lifetime value curves by cohort ltv Cumulative LTV
Time-to-event / survival analysis survival, life_table Survival records
Behavior before and after an event (event study) event_panel Event panels
Repeated eligibility for observational "trials" trial_panel Trial panels
State changes such as plan upgrades and downgrades state_panel State panels
Ordered events in a window after an event, up to a set count event_sequence Event sequences

More feature recipes are in common feature patterns: recency, "active in the last N days" filters, attributes as of t0, conversions after a purchase, and true/false labels.

Where it runs

Queries run where your data lives; nothing is uploaded. DataFrames and Arrow tables, as above, are queried in a private in-process DuckDB. For a warehouse, pass its Ibis connection first and name the tables: tr.Model.from_tables(con, units={"customer": tr.UnitSource("customers", "customer_id")}, events={"orders": tr.Events("orders", ...)}). Results come back as pandas, Polars or Arrow.

Backend Status
DuckDB Reference backend; can also save versioned datasets
PostgreSQL 16, BigQuery, Snowflake Verified against DuckDB; floating-point results match within documented tolerances
Other Ibis backends Read-only, unverified; opt in with allow_unverified=True

See backend compatibility for extras and details.

Learn more

Contributing? See CONTRIBUTING.md.

License

Apache License 2.0; see NOTICE.

Metadata

Release files for terrine 0.1.0.dev2

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for terrine 0.1.0.dev2
File Size Uploaded Provenance Actions
terrine-0.1.0.dev2.tar.gz 1.4 MB With provenance Details for terrine-0.1.0.dev2.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for terrine 0.1.0.dev2
File Interpreter ABI Platform Actions
terrine-0.1.0.dev2-py3-none-any.whl Python 3 none any Details for terrine-0.1.0.dev2-py3-none-any.whl

Total release size: 1.8 MB

Release files / terrine-0.1.0.dev2.tar.gz

Download URL terrine-0.1.0.dev2.tar.gz
Size 1.4 MB
Tags Source
SHA-256 checksum
How to use checksums
0f02eccf6479012add27f789f1680b129ad067ec30d631b55ec24eae89d8d334
BLAKE2b-256 checksum
How to use checksums
57227e7e8c9196286b7922ba5b3bf60f12a606cd505d7db56dfd388cf058fc3e
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 29, 2026.

Transparency log

Release files / terrine-0.1.0.dev2-py3-none-any.whl

Download URL terrine-0.1.0.dev2-py3-none-any.whl
Size 466.8 kB
Tags Python 3
SHA-256 checksum
How to use checksums
0a6b4a0eaf9cc2c868d8f9eb47cd6ffcac7945709eab44e335b6f7fe18dd90c5
BLAKE2b-256 checksum
How to use checksums
9c211d0da53944566b4c0836231dc4b5a6333e00c07821911c852b3d7430a3d4
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 29, 2026.

Transparency log

Security for terrine 0.1.0.dev2

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for terrine-0.1.0.dev2.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 29, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

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