Define the model features as attributes
The features for your model are Signals attributes. You'll define them once in this step, and use the same published definition for training and serving.
You can create the definition with the Signals Python SDK, or by describing it to the Snowplow Assistant in Console or to your own AI assistant connected to the Snowplow MCP server. The rest of the tutorial is in Python either way, because it builds the dataset and trains the model in code.
Decide on the features
The model predicts whether the current session will end in a purchase, so the features describe the current session. You'll calculate them against the domain_sessionid attribute key. The dataset builder requires this key when it generates session anchors.
This starter set counts the main shopping actions and measures how long the session has lasted:
| Attribute | Description | Aggregation |
|---|---|---|
page_views | Page views in the session | counter |
product_views | Product detail views in the session | counter |
add_to_carts | Cart additions in the session | counter |
remove_from_carts | Cart removals in the session | counter |
checkout_steps | Checkout steps reached in the session | counter |
minutes_in_session | Minutes since the first event in the session | time_since_first |
All the ecommerce actions are the same snowplow_ecommerce_action event, so each counter filters on the event's type property. Signals matches events by exact version, so check which version of snowplow_ecommerce_action your tracker sends, for example in the event_version column of atomic.events. With the sample data, use 1-0-2: the sample table and the shopper script both use it.
Choose features that exist at prediction time
A feature is only useful if it has a value when you score. Two kinds of attribute cause problems:
- Attributes that only change after the outcome, such as a count of
transactionevents in the session: the dataset builder only places anchors before a session's first purchase, so this column would always be empty in training - Attributes whose meaning depends on exactly when you read them, such as
time_since_laston all events: in training it measures the gap before the anchor event, but if you score right after an event it's close to zero every time
Counts and session duration don't have these problems, which is why the starter set uses them. Once the pipeline works, try adding attributes such as the number of distinct products viewed or the value of items added to the cart, and keep the ones that improve the evaluation metrics in Train the model.
Connect to Signals
You'll use the Signals Python SDK from the next step onwards, whichever way you create the definition. The Signals client takes four values:
- The Signals API URL and your organization ID, from Signals > Overview in Snowplow Console
- An API key and API key ID, which you create in Console
See Connect to Signals for details. Set the values as environment variables, then create the client:
import os
from snowplow_signals import Signals
sp_signals = Signals(
api_url=os.environ["SP_API_URL"], # includes https://
api_key=os.environ["SP_API_KEY"],
api_key_id=os.environ["SP_API_KEY_ID"],
org_id=os.environ["SP_ORG_ID"],
)
Create and publish the attribute group
Create an attribute group called purchase_propensity with the six attributes, and a service called purchase_propensity_service to retrieve them from your /predict endpoint later. Keep these names: the code in the rest of the tutorial uses them.
- Python SDK
- Snowplow Assistant or MCP
Define the attributes. A helper function builds the five ecommerce counters, which differ only in name and action type:
from snowplow_signals import (
Attribute,
Criteria,
Criterion,
Event,
EventProperty,
PageView,
)
# UPDATE THIS if your tracker sends a different version of the event
ecommerce_action = Event(
vendor="com.snowplowanalytics.snowplow.ecommerce",
name="snowplow_ecommerce_action",
version="1-0-2",
)
def ecommerce_counter(name: str, action_type: str) -> Attribute:
return Attribute(
name=name,
type="int32",
events=[ecommerce_action],
aggregation="counter",
criteria=Criteria(
all=[
Criterion.eq(
EventProperty(
vendor="com.snowplowanalytics.snowplow.ecommerce",
name="snowplow_ecommerce_action",
major_version=1,
path="type",
),
action_type,
)
]
),
)
attributes = [
Attribute(
name="page_views",
type="int32",
events=[PageView()],
aggregation="counter",
),
ecommerce_counter("product_views", "product_view"),
ecommerce_counter("add_to_carts", "add_to_cart"),
ecommerce_counter("remove_from_carts", "remove_from_cart"),
ecommerce_counter("checkout_steps", "checkout_step"),
Attribute(
name="minutes_in_session",
type="double",
events=[],
aggregation="time_since_first",
time_unit="min",
),
]
Group the attributes and create the service. Use your own email address for owner.
from snowplow_signals import Service, StreamAttributeGroup, domain_sessionid
propensity_group = StreamAttributeGroup(
name="purchase_propensity",
version=1,
owner="your_email@example.com", # UPDATE THIS
attribute_key=domain_sessionid,
attributes=attributes,
)
propensity_service = Service(
name="purchase_propensity_service",
owner="your_email@example.com", # UPDATE THIS
attribute_groups=[propensity_group],
)
Before publishing, you can test what the group calculates on the last hour of events in your warehouse:
sp_signals.test(attribute_group=propensity_group)
Publish the attribute group and service:
published = sp_signals.publish([propensity_group, propensity_service])
print(f"{len(published)} objects published")
Open the Snowplow Assistant in Console, or an assistant connected to the Snowplow MCP server. The conversational Signals tutorial explains both. Then describe the attribute group and service:
Create a stream attribute group called purchase_propensity, keyed on
domain_sessionid, with these six attributes and no period:
1. page_views: count of page_view events
2. product_views: count of snowplow_ecommerce_action events where type is
product_view
3. add_to_carts: the same, where type is add_to_cart
4. remove_from_carts: the same, where type is remove_from_cart
5. checkout_steps: the same, where type is checkout_step
6. minutes_in_session: minutes since the first event of any kind in the
session
Use the snowplow_ecommerce_action version my tracking sends. Then create a
service called purchase_propensity_service with only this group. Save both
as drafts, don't publish yet.
Ask the assistant to show the full definition, and check that:
- The attribute key is
domain_sessionid - The five counters use the
counteraggregation, and the four ecommerce ones filter on thetypeproperty ofsnowplow_ecommerce_action minutes_in_sessionusestime_since_firstwith themintime unit, on all events- No attribute has a period
If anything is off, say what to change in plain language. Then test the group against recent events:
Test purchase_propensity against recent events before we publish.
When the definition and test results look right, publish both:
Looks good. Publish purchase_propensity and purchase_propensity_service.
From this point, Signals calculates the attributes in real time for every new session on your site. Stream attribute groups only process events received after publishing.
Publishing doesn't affect the training dataset. The dataset builder computes historical values directly from atomic.events, so you don't have to wait for Signals to accumulate data before you train.
Check the published group
In Snowplow Console, go to Signals > Attribute groups. You should see purchase_propensity at version 1 with a published status. Open it and check the six attributes against the table above.
The next steps load this published definition from Signals by name, rather than reusing the object in your notebook. That way the dataset is always built from the definition Signals serves, however you created it.
Troubleshooting
- The test returns no rows, or the ecommerce counters are always empty: check that
ecommerce_actionuses the same version as theevent_versionyour tracker sends, and that there were ecommerce events in the last hour. UnsupportedProtocolorConnectErrorwhen creating the client:SP_API_URLmust be the full URL, includinghttps://.