diff --git a/docs/signals/ml-training-datasets/index.md b/docs/signals/ml-training-datasets/index.md new file mode 100644 index 000000000..96ba081a3 --- /dev/null +++ b/docs/signals/ml-training-datasets/index.md @@ -0,0 +1,285 @@ +--- +title: "Create ML training datasets" +sidebar_position: 32 +sidebar_label: "ML training datasets" +description: "Build labeled, point-in-time correct training datasets for machine learning models using the Signals Python SDK. Define anchor events, build datasets, and retrieve results." +keywords: ["training datasets", "machine learning", "dataset builder", "anchors", "signals python sdk", "ML"] +date: "2026-07-15" +--- + +The Signals dataset builder lets you create labeled training datasets for machine learning models directly from your Snowplow event data. It generates point-in-time correct features from your existing [attribute groups](/docs/signals/attributes/attribute-groups/index.md), so the features your model trains on are identical to the features it receives at inference time. + +Training a machine learning model on behavioral data requires a dataset where each row represents a moment in time, with features computed only from events that had occurred up to that point. Building these datasets manually is error-prone: it is easy to accidentally include future information (data leakage), which produces models that perform well in testing but fail in production. The dataset builder automates this process, enforcing point-in-time correctness by construction. + +To follow along with a working example, open the [dataset builder notebook](https://colab.research.google.com/github/snowplow/documentation/blob/signals-dataset-builder/static/notebooks/signals-dataset-builder.ipynb) in Google Colab. + +Start by [connecting to Signals](/docs/signals/connection/index.md) to create a `Signals` client object. + +## Why point-in-time correctness matters + +When you serve predictions in real time, your model only has access to events that have happened so far in the session. If your training data includes attributes computed from the full session (including events after the prediction point), your model learns patterns it will never see in production. This is called data leakage, and it is the most common reason ML models degrade after deployment. + +The dataset builder prevents this by computing each attribute value using only events that occurred before the anchor timestamp. Because it uses the same attribute group definitions that power your real-time Signals deployment, training and serving are guaranteed to use the same feature logic. + +## Workflow + +Building and using an ML training dataset follows this process: + +1. [Define your attribute groups](/docs/signals/attributes/attribute-groups/index.md) with the features you want your model to learn from (e.g. `product_view_count`, `add_to_cart_count`) +2. Submit a dataset run by calling the appropriate method - `submit_dataset_run_with_session_anchors()` for auto-generated anchors, or `submit_dataset_run_with_custom_anchors()` for a pre-existing anchor table +3. Poll for completion with `get_dataset_run_status()`, then retrieve a preview with `get_dataset_run_preview()` +4. Train your model on the resulting DataFrame +5. Deploy your model, serving it the same attributes in real time via [Retrieve attributes](/docs/signals/applications/retrieve-attributes/index.md) + +## How it works + +The dataset builder produces a training dataset in three stages: + +```mermaid +flowchart LR + subgraph stage1["1. Anchors"] + direction TB + A1["Scan sessions in
training window"] --> A2{"Goal event
occurred?"} + A2 -- Yes --> A3["Positive anchor
(label = 1)"] + A2 -- No --> A4["Negative anchor
(label = 0)"] + A3 --> A5["Downsample
negatives"] + A4 --> A5 + end + + subgraph stage2["2. Attributes"] + direction TB + B1["For each anchor,
find preceding events"] --> B2["Compute attribute
values using only
events before
anchor timestamp"] + end + + subgraph stage3["3. Assembly"] + direction TB + C1["Join anchors +
attributes into
labeled dataset"] --> C2["One row per anchor
One column per attribute"] + end + + stage1 --> stage2 --> stage3 +``` + +1. Anchors: scan sessions within the training window and identify anchor points. Sessions where the goal event occurred produce positive anchors (label=1). Sessions without the goal produce negative anchors (label=0). Negative anchors are downsampled to avoid class imbalance. +2. Attributes: for each anchor, compute attribute values using only the events that preceded the anchor timestamp in that session. This enforces point-in-time correctness, so attributes reflect only what was known at the moment of the anchor. +3. Assembly: join anchors with their computed attribute values into a single labeled dataset, with one row per anchor and one column per attribute. + +For example, if you define two attributes (`product_view_count` and `add_to_cart_count`) with a transaction goal, the final dataset looks like this: + +| domain_sessionid | anchor_ts | label | product_view_count | add_to_cart_count | +| --- | --- | --- | --- | --- | +| `abc-123` | 2024-01-15 09:32:00 | 1 | 5 | 2 | +| `def-456` | 2024-01-15 10:01:00 | 0 | 3 | 0 | +| `ghi-789` | 2024-01-16 14:22:00 | 1 | 8 | 4 | + +Each row captures the attribute values as they were at the anchor timestamp, not at the end of the session. A model trained on this data learns the same signal it will see when serving predictions in real time. + +## Define anchors + +Anchors are the labeled events that form the rows of your training dataset. Each anchor is a point in a session that receives a label: `1` if the user achieved the goal, `0` if they did not. + +Signals provides two approaches: session anchors (automatically derived from your event data) and user-supplied anchors (from a table you provide). You choose between them by calling different methods on the Signals client. + +### Session anchors + +Use `submit_dataset_run_with_session_anchors()` to automatically generate anchors from your event data and build the training dataset server-side. You specify a goal (the criteria that define a positive outcome) and a time window to scan. Signals scans all sessions in the training window and labels each based on whether the goal was achieved. For positive sessions, anchors are placed at the goal event itself. For negative sessions (where the goal was never achieved), anchors are placed at randomly selected events within the session. Negative anchors are then downsampled according to `max_negative_ratio` to avoid class imbalance. + +The `goal_criteria` argument uses the same `Criteria` and `Criterion` classes as [attribute criteria](/docs/signals/attributes/attributes/index.md). You can filter on atomic fields, event properties, or entity properties. + +To match on an event name (an atomic field): + +```python +from datetime import datetime, timezone + +from snowplow_signals import ( + AtomicProperty, + Criteria, + Criterion, + TrainingSpan, +) + +run = sp_signals.submit_dataset_run_with_session_anchors( + attribute_groups=[my_attribute_group], + goal_criteria=Criteria( + all=[ + Criterion.eq(AtomicProperty(name="event"), "transaction"), + ] + ), + training_span=TrainingSpan( + start_time=datetime(2024, 1, 1, tzinfo=timezone.utc), + end_time=datetime(2024, 4, 1, tzinfo=timezone.utc), + ), +) +``` + +To filter on a property within a self-describing event: + +```python +from snowplow_signals import Criteria, Criterion, EventProperty, TrainingSpan + +run = sp_signals.submit_dataset_run_with_session_anchors( + attribute_groups=[my_attribute_group], + goal_criteria=Criteria( + all=[ + Criterion.eq( + EventProperty( + vendor="com.snowplowanalytics.snowplow.ecommerce", + name="snowplow_ecommerce_action", + major_version=1, + path="type", + ), + "transaction", + ), + ] + ), + training_span=TrainingSpan( + start_time=datetime(2024, 1, 1, tzinfo=timezone.utc), + end_time=datetime(2024, 4, 1, tzinfo=timezone.utc), + ), +) +``` + +| Argument | Description | Type | Required? | +| --- | --- | --- | --- | +| `attribute_groups` | Attribute groups that provide the feature columns. Each attribute in these groups becomes a column in the final dataset. | `list[AttributeGroup]` | ✅ | +| `goal_criteria` | Criteria that define a positive anchor (label=1) | `Criteria` | ✅ | +| `training_span` | Time window to scan for anchor events | `TrainingSpan` | ✅ | +| `min_events` | Minimum number of prior in-session events before an anchor is eligible. Increase this to filter out anchors with too little behavioral signal, for example set to `5` to ensure each anchor has at least 5 prior events. | `int` | Default: `1` | +| `max_anchors_per_session` | Maximum anchor events per session. `None` for unlimited. Set this to limit overrepresentation of long sessions, for example set to `1` to ensure each session contributes at most one training example. | `int` or `None` | Default: `None` | +| `max_negative_ratio` | Maximum ratio of negative to positive anchors. Negative anchors are downsampled to this ratio. Lower values produce more balanced datasets; higher values preserve more data. For example, set to `1.0` for a balanced 1:1 dataset. | `float` | Default: `5.0` | +| `excluded_events` | Events to exclude from anchor generation. By default, `page_ping` events are excluded because they do not represent meaningful user actions. | `list` | Default: `page_ping` events excluded | +| `anchors_table` | Override the output location for the anchors table. Only the `table` field is required - `database` and `schema` default to the output database and schema from your [Signals warehouse connection](/docs/signals/setup/index.md). The default table name is `signals_anchors`. | `WarehouseTable` | Default: `None` | +| `attributes_table` | Override the output location for the intermediate attribute tables. During execution, one table is created per attribute key (e.g. `signals_attributes_domain_sessionid`), joining each anchor with its point-in-time attribute values. Supports `database`, `schema`, and `table_prefix` fields - all default to the output database and schema from your [Signals warehouse connection](/docs/signals/setup/index.md). | `AttributesWarehouseTable` | Default: `None` | +| `dataset_table` | Override the output location for the final assembled dataset. Only the `table` field is required - `database` and `schema` default to the output database and schema from your [Signals warehouse connection](/docs/signals/setup/index.md). The default table name is `signals_training_dataset`. | `WarehouseTable` | Default: `None` | +| `max_lookback_days` | How far back from each anchor timestamp to look for events when computing attributes. By default, this is derived from the longest period defined across your attributes. Set a lower value to narrow the event window, or a higher value to include older events. | `int` | Default: derived from attribute periods | + +#### Customizing output tables + +By default, the dataset builder creates tables using the output database and schema from your [Signals warehouse connection](/docs/signals/setup/index.md). To write tables to a different location, pass a `WarehouseTable`. Only `table` is required - `database` and `schema` are optional overrides. + +```python +from snowplow_signals import WarehouseTable + +run = sp_signals.submit_dataset_run_with_session_anchors( + ... + dataset_table=WarehouseTable( + table="my_training_dataset", # required + database="analytics", # optional, defaults to Signals connection + schema="ml", # optional, defaults to Signals connection + ), +) +``` + +### User-supplied anchors + +If you already have a table of labeled anchor events, use `submit_dataset_run_with_custom_anchors()` instead. Your table must contain the following columns: + +| Column | Type | Description | +| --- | --- | --- | +| Attribute key column (e.g. `domain_sessionid`) | `VARCHAR` | The attribute key used by your attribute groups. The column name must match the attribute key name. | +| `anchor_ts` | `TIMESTAMP` | The timestamp of the anchor event | +| `label` | `INTEGER` | `1` for positive, `0` for negative. Only required when `anchors_have_label` is `True`. | + +For example, if your attribute groups use `domain_sessionid` as the attribute key: + +| domain_sessionid | anchor_ts | label | +| --- | --- | --- | +| `abc-123` | 2024-01-15 09:32:00 | 1 | +| `def-456` | 2024-01-15 10:01:00 | 0 | + +```python +from snowplow_signals import WarehouseTable + +run = sp_signals.submit_dataset_run_with_custom_anchors( + attribute_groups=[my_attribute_group], + anchors_table=WarehouseTable( + database="analytics", + schema="ml", + table="my_anchor_events", + ), + anchors_have_label=True, +) +``` + +| Argument | Description | Type | Required? | +| --- | --- | --- | --- | +| `attribute_groups` | Attribute groups that provide the feature columns. Each attribute in these groups becomes a column in the final dataset. | `list[AttributeGroup]` | ✅ | +| `anchors_table` | Table containing your pre-built anchor events | `WarehouseTable` | ✅ | +| `anchors_have_label` | Whether the source table contains a `label` column. Set to `False` if your anchors are unlabeled. | `bool` | Default: `True` | +| `attributes_table` | Override the output location for the intermediate attribute tables. During execution, one table is created per attribute key (e.g. `signals_attributes_domain_sessionid`), joining each anchor with its point-in-time attribute values. Supports `database`, `schema`, and `table_prefix` fields - all default to the output database and schema from your [Signals warehouse connection](/docs/signals/setup/index.md). | `AttributesWarehouseTable` | Default: `None` | +| `dataset_table` | Override the output location for the final assembled dataset. Only the `table` field is required - `database` and `schema` default to the output database and schema from your [Signals warehouse connection](/docs/signals/setup/index.md). | `WarehouseTable` | Default: `None` | +| `max_lookback_days` | How far back from each anchor timestamp to look for events when computing attributes. By default, this is derived from the longest period defined across your attributes. | `int` | Default: derived from attribute periods | + +## Poll for completion and retrieve results + +The `submit_dataset_run_with_session_anchors()` and `submit_dataset_run_with_custom_anchors()` methods return a `DatasetRunResponse` immediately. The dataset is built server-side, so you need to poll for completion before retrieving results. + +### Check run status + +Use `get_dataset_run_status()` to check whether the run has finished. The `status` field is one of `pending`, `success`, or `failed`. + +```python +status = sp_signals.get_dataset_run_status(run.id) +print(status.status) # "pending", "success", or "failed" +``` + +To wait for completion in a notebook or script: + +```python +import time +from snowplow_signals import DatasetRunStatus + +while True: + status = sp_signals.get_dataset_run_status(run.id) + if status.status == DatasetRunStatus.SUCCESS: + break + if status.status == DatasetRunStatus.FAILED: + raise RuntimeError("Dataset run failed") + time.sleep(5) +``` + +### Get results as a DataFrame + +Once the run status is `success`, call `get_dataset_run_preview()` to fetch a preview of the completed dataset. By default, it returns up to 100 rows. You can set `limit` up to 10,000. + +```python +preview = sp_signals.get_dataset_run_preview(run.id, limit=10000) +df = preview.to_pandas() +``` + +The resulting DataFrame contains one row per anchor, with columns for the attribute key, anchor timestamp, label, and every attribute from your attribute groups: + +| | domain_sessionid | anchor_ts | label | product_view_count | add_to_cart_count | +| --- | --- | --- | --- | --- | --- | +| 0 | abc-123 | 2024-01-15 09:32:00 | 1 | 5 | 2 | +| 1 | def-456 | 2024-01-15 10:01:00 | 0 | 3 | 0 | +| 2 | ghi-789 | 2024-01-16 14:22:00 | 1 | 8 | 4 | + +The full dataset is also written to your warehouse at the table location shown in `run.dataset`. You can query this table directly for the complete result set. + +### Cancel a run + +To cancel a dataset build that is still in progress: + +```python +sp_signals.cancel_dataset_run(run.id) +``` + +## Inspect the generated SQL + +If you want to review the SQL that the dataset builder produces without executing it, use `build_dataset_with_session_anchors()` or `build_dataset_with_custom_anchors()`. These return a `DatasetBundle` containing the generated SQL files, which you can save to disk for inspection or manual execution. + +```python +bundle = sp_signals.build_dataset_with_session_anchors( + attribute_groups=[my_attribute_group], + goal_criteria=goal, + training_span=span, +) + +bundle.save_to("./dataset_output") +``` + +This creates: + +- Individual SQL files for each stage (`signals_anchors.sql`, `signals_attributes_domain_sessionid.sql`, `signals_training_dataset.sql`) +- `manifest.json` with input configuration and output table mappings +- `README.md` documenting the execution order diff --git a/static/notebooks/signals-dataset-builder.ipynb b/static/notebooks/signals-dataset-builder.ipynb new file mode 100644 index 000000000..40fc0611b --- /dev/null +++ b/static/notebooks/signals-dataset-builder.ipynb @@ -0,0 +1,189 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "656e9de5", + "metadata": {}, + "source": "# Signals dataset builder - ML training dataset example\n\nThis notebook walks through the Signals **dataset builder**: turning your Snowplow event data into a **labeled, point-in-time-correct** training dataset for a machine learning model.\n\n**Scenario:** predict whether an ecommerce session will convert (a `transaction`) from behaviour seen *so far* in the session. Features are computed from the same attribute-group definitions that power real-time Signals, so training and serving use identical feature logic.\n\n**Why point-in-time correctness matters:** at serving time the model only sees events that have happened so far in the session. The builder computes each feature using only events *before* the anchor timestamp, which prevents data leakage (the most common cause of models that test well but fail in production).\n\n**Workflow:**\n1. Define attribute groups with the features to learn from\n2. Submit a dataset run (session anchors or custom anchors)\n3. Poll for completion and retrieve results\n4. Optionally inspect / save the generated SQL\n\nDocs: https://docs.snowplow.io/docs/signals/ml-training-datasets/" + }, + { + "cell_type": "markdown", + "id": "b0933ff8", + "metadata": {}, + "source": [ + "## 1. Install the SDK" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "063a017f", + "metadata": {}, + "outputs": [], + "source": "%pip install snowplow-signals==0.4.7 pandas" + }, + { + "cell_type": "markdown", + "id": "ec80246b", + "metadata": {}, + "source": "## 2. Connect to Signals\n\nLoads the four Signals credentials from [Google Colab secrets](https://x.com/GoogleColab/status/1719798406195867814) and creates the `Signals` client. The client submits dataset runs and retrieves results via the Signals API.\n\nAdd the following secrets in the Colab sidebar (key icon): `SP_API_URL`, `SP_API_KEY`, `SP_API_KEY_ID`, `SP_ORG_ID`." + }, + { + "cell_type": "code", + "execution_count": null, + "id": "62cec6fd", + "metadata": {}, + "outputs": [], + "source": "from google.colab import userdata\nfrom snowplow_signals import Signals\n\nsp_signals = Signals(\n api_url=userdata.get(\"SP_API_URL\"),\n api_key=userdata.get(\"SP_API_KEY\"),\n api_key_id=userdata.get(\"SP_API_KEY_ID\"),\n org_id=userdata.get(\"SP_ORG_ID\"),\n)\nsp_signals" + }, + { + "cell_type": "markdown", + "id": "ff99684e", + "metadata": {}, + "source": "## 3. Define the features\n\nEach attribute becomes a column in the training dataset. Here we count product views and add-to-cart actions within the session, filtered from the ecommerce action event by its `type` property.\n\nYou don't need to publish these to build a dataset - the builder works directly from the definitions. Publishing is what you'd do later to serve the same features in real time." + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8cdb5e83", + "metadata": {}, + "outputs": [], + "source": [ + "from datetime import timedelta\n", + "from snowplow_signals import Attribute, Event, EventProperty, Criteria, Criterion\n", + "\n", + "# The Snowplow ecommerce action event carries an action `type` (product_view, add_to_cart, transaction, ...)\n", + "ecommerce_action = Event(\n", + " name=\"snowplow_ecommerce_action\",\n", + " vendor=\"com.snowplowanalytics.snowplow.ecommerce\",\n", + " version=\"1-0-2\",\n", + ")\n", + "\n", + "\n", + "def action_type_is(value: str) -> Criteria:\n", + " \"\"\"Filter ecommerce action events to a single action type.\"\"\"\n", + " return Criteria(\n", + " all=[\n", + " Criterion.eq(\n", + " EventProperty(\n", + " vendor=\"com.snowplowanalytics.snowplow.ecommerce\",\n", + " name=\"snowplow_ecommerce_action\",\n", + " major_version=1,\n", + " path=\"type\",\n", + " ),\n", + " value,\n", + " )\n", + " ]\n", + " )\n", + "\n", + "\n", + "product_view_count = Attribute(\n", + " name=\"product_view_count\",\n", + " description=\"Product views in the session so far\",\n", + " events=[ecommerce_action],\n", + " aggregation=\"counter\",\n", + " type=\"int32\",\n", + " period=timedelta(hours=1),\n", + " criteria=action_type_is(\"product_view\"),\n", + ")\n", + "\n", + "add_to_cart_count = Attribute(\n", + " name=\"add_to_cart_count\",\n", + " description=\"Add-to-cart actions in the session so far\",\n", + " events=[ecommerce_action],\n", + " aggregation=\"counter\",\n", + " type=\"int32\",\n", + " period=timedelta(hours=1),\n", + " criteria=action_type_is(\"add_to_cart\"),\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "855643d4", + "metadata": {}, + "source": [ + "### Group the features\n", + "\n", + "Attributes live inside an attribute group, keyed by `domain_sessionid` so features are computed per session." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5405b93b", + "metadata": {}, + "outputs": [], + "source": [ + "from snowplow_signals import StreamAttributeGroup, domain_sessionid\n", + "\n", + "ecommerce_group = StreamAttributeGroup(\n", + " name=\"ecommerce_session_features\",\n", + " version=1,\n", + " attribute_key=domain_sessionid,\n", + " owner=\"jack.keene@snowplowanalytics.com\",\n", + " attributes=[product_view_count, add_to_cart_count],\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "4180b0e9", + "metadata": {}, + "source": "## 4. Submit a dataset run with session anchors\n\n`submit_dataset_run_with_session_anchors()` submits a dataset build for server-side execution. It scans every session in the training window and labels it: sessions where the **goal** (a `transaction`) occurred get a positive anchor (label=1) at the goal event; sessions without it get negative anchors (label=0) at random events, then downsampled to `max_negative_ratio`.\n\nKey knobs used below:\n- `min_events=3` - skip anchors with too little prior signal\n- `max_anchors_per_session=10` - up to 10 training rows per session, so long sessions don't dominate\n- `max_negative_ratio=1.0` - a balanced 1:1 positive/negative dataset\n\nThis returns a `DatasetRunResponse` immediately. The dataset is built asynchronously on the server." + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9351713a", + "metadata": {}, + "outputs": [], + "source": "from datetime import datetime, timezone\nfrom snowplow_signals import TrainingSpan\n\nrun = sp_signals.submit_dataset_run_with_session_anchors(\n attribute_groups=[ecommerce_group],\n goal_criteria=action_type_is(\"transaction\"),\n training_span=TrainingSpan(\n start_time=datetime(2024, 1, 1, tzinfo=timezone.utc),\n end_time=datetime(2024, 4, 1, tzinfo=timezone.utc),\n ),\n min_events=3,\n max_anchors_per_session=10,\n max_negative_ratio=1.0,\n)\nrun" + }, + { + "cell_type": "markdown", + "id": "0959fc83", + "metadata": {}, + "source": "## 5. Poll for completion\n\nThe dataset is built server-side. Poll `get_dataset_run_status()` until the status is `success` or `failed`." + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ba6d06e3", + "metadata": {}, + "outputs": [], + "source": "import time\nfrom snowplow_signals import DatasetRunStatus\n\nwhile True:\n status = sp_signals.get_dataset_run_status(run.id)\n print(f\"Status: {status.status}\")\n if status.status == DatasetRunStatus.SUCCESS:\n break\n if status.status == DatasetRunStatus.FAILED:\n raise RuntimeError(\"Dataset run failed\")\n time.sleep(5)" + }, + { + "cell_type": "markdown", + "id": "7c238640", + "metadata": {}, + "source": "## 6. Get the training dataset as a DataFrame\n\n`get_dataset_run_preview()` fetches a preview of the completed dataset. Call `to_pandas()` on the result to get a DataFrame (one row per anchor; columns for the attribute key, anchor timestamp, label, and every attribute). Set `limit` up to 10,000 rows." + }, + { + "cell_type": "code", + "execution_count": null, + "id": "35923045", + "metadata": {}, + "outputs": [], + "source": "preview = sp_signals.get_dataset_run_preview(run.id, limit=10000)\ndf = preview.to_pandas()\nprint(df.shape)\ndf.head()" + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": "## Alternative: user-supplied anchors\n\nIf you already have a labeled anchor table (columns: your attribute key e.g. `domain_sessionid`, `anchor_ts`, and optionally `label`), use `submit_dataset_run_with_custom_anchors()` instead of the session-anchor method. Everything downstream (polling, preview, `to_pandas`) is identical.\n\n```python\nfrom snowplow_signals import WarehouseTable\n\nrun = sp_signals.submit_dataset_run_with_custom_anchors(\n attribute_groups=[ecommerce_group],\n anchors_table=WarehouseTable(\n database=\"analytics\",\n schema=\"ml\",\n table=\"my_anchor_events\",\n ),\n anchors_have_label=True,\n)\n```\n\n## Inspect the generated SQL\n\nIf you want to review the SQL before execution, use `build_dataset_with_session_anchors()` to generate a `DatasetBundle` and save it to disk:\n\n```python\nbundle = sp_signals.build_dataset_with_session_anchors(\n attribute_groups=[ecommerce_group],\n goal_criteria=action_type_is(\"transaction\"),\n training_span=TrainingSpan(\n start_time=datetime(2024, 1, 1, tzinfo=timezone.utc),\n end_time=datetime(2024, 4, 1, tzinfo=timezone.utc),\n ),\n)\nbundle.save_to(\"./dataset_output\")\n```" + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file