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
- Your first dataset: this example step by step, with output limits and saved datasets.
- Training and scoring sets: reproducible train/score tables and saved definitions.
- Library hand-offs: pass datasets to modeling libraries. Terrine builds data and makes no causal claims.
- All guides, limitations and error codes.
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)
| 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)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| 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 logRelease 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 logSecurity 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.
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 logA 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.