Add Segment
Add a new categorical segment column to the eventstream.
Exactly one of rules, func, sql, funnel_events, time_range, or
metric_bins must be provided — unless name is already listed in
schema.custom_cols, in which case passing none of them promotes that
existing column to a segment in place, without recomputing its values.
Usage
# ordered CASE-WHEN rules over an existing column
stream.add_segment("region", rules=[
["country", "=", "US", "domestic"],
["country", "in", "('GB', 'DE', 'FR')", "europe"],
["other"],
])
# the deepest funnel step each path completed in order
stream.add_segment("funnel", funnel_events=["add_to_cart", "shipping_details", "purchase"])
# "inside" / "outside" a time window
stream.add_segment("incident", time_range=("2024-03-10", "2024-03-17"))
# bin paths by a metric — named bins with explicit cut points ...
stream.add_segment("path_length", metric_bins={
"metric": {"metric": "length"},
"edges": [5, 15],
"segment_levels": ["short", "mid", "long"],
})
# ... or quartiles, with q1..q4 named for you
stream.add_segment("speed", metric_bins={
"metric": {"metric": "duration"},
"quantiles": 4,
})
# a DuckDB SELECT returning one label per row
stream.add_segment("device", sql="SELECT CASE WHEN platform = 'mobile' THEN 'mobile' ELSE 'web' END FROM eventstream")
# promote a column that is already in the eventstream, keeping its values
stream.add_segment("returned")
How it works
A segment column stores one label per event row, which is what lets a segment be either static (the same label along a whole path) or dynamic (changing as the path goes on). See Segments for what that distinction buys you.
rules — CASE-WHEN over existing columns
Conditions are tried in order; the last entry is the fallback label.
Before — a country column that came along with the source data:
| user_id | event | country |
|---|---|---|
| u1 | home | US |
| u1 | cart | US |
| u2 | home | DE |
| u2 | cart | DE |
stream.add_segment("region", rules=[
["country", "=", "US", "domestic"],
["international"],
])
After:
| user_id | event | country | region |
|---|---|---|---|
| u1 | home | US | domestic |
| u1 | cart | US | domestic |
| u2 | home | DE | international |
| u2 | cart | DE | international |
Write string values unquoted — "US", not "'US'" — since they get quoted
for you. The one exception is op="in", whose value is passed through to SQL
as-is and so must be a complete tuple:
["country", "in", "('GB', 'DE', 'FR')", "europe"].
funnel_events — how deep did this path get
Labels each path with the deepest funnel step it completed in order. This is the mode that turns a funnel drop-off into a comparable group.
Before — four paths against an add_to_cart → checkout → purchase funnel:
- u1:
home → add_to_cart → checkout → purchase - u2:
home → add_to_cart → checkout - u3:
home → checkout → purchase(skippedadd_to_cart) - u4:
home
stream.add_segment("funnel", funnel_events=["add_to_cart", "checkout", "purchase"])
After — one label per path, written onto every one of its events:
| path | funnel |
|---|---|
| u1 | purchase |
| u2 | checkout |
| u3 | out_of_funnel |
| u4 | out_of_funnel |
Note u3: it reached both checkout and purchase, but never completed
add_to_cart first, so the strictly ordered funnel gives it no credit at all.
Paths that never complete even the first step are labelled out_of_funnel.
time_range — inside vs outside a window
stream.add_segment("incident", time_range=("2024-03-10", "2024-03-17"))
Every event is labelled inside or outside by its own timestamp, so a user
active both during and after an incident contributes to both groups. That is
what makes this the right tool for "how did behaviour change during X?" — see
Segments.
metric_bins — split by a per-path metric
stream.add_segment("path_length", metric_bins={
"metric": {"metric": "length"},
"edges": [5, 15],
"segment_levels": ["short", "mid", "long"],
})
Any path metric that yields one value per path can become
a segment. edges are interior cut points: two of them always give three
bins — < 5, 5-15, >= 15 — so every path lands somewhere. (This is
deliberately unlike pandas.cut, where the list is the outer edges and anything
outside becomes NaN. A segment column has no room for NaN: diff and
in_segment both need every path to carry a level.)
Swap edges for quantiles when you don't know the distribution in advance —
an int asks for that many equal-sized bins, a list gives the cut quantiles:
stream.add_segment("speed", metric_bins={
"metric": {"metric": "duration"},
"quantiles": 4, # or [0.25, 0.75]
})
Quantile boundaries are computed over the eventstream as it is at this call,
so add_segment(...).filter_paths(...) and filter_paths(...).add_segment(...)
give different boundaries — put the split where you want the population defined.
segment_levels is optional; without it the bins are named after themselves
("[5, 15)") or q1..qN. With it, there is one name per bin, so its length is
always one more than the number of cut points. A path the metric has no value
for gets the level "undefined", which is not one of the bins — rename it with
rename_segment_levels if that
name is in your way.
Binning is what makes a continuous confounder usable as a control: comparing two groups within buckets of path length says something the pooled comparison can't, because path length is itself correlated with most outcomes.
func and sql — anything else
func receives the raw DataFrame and returns one label per row, in row order;
sql is a DuckDB SELECT over the eventstream alias returning a single
column, also one value per row in order.
Promoting an existing column
If the column is already in the eventstream — it rode along from the source data
as a custom column — call add_segment with just the name and no mode argument.
Its values are kept as they are and the column becomes a segment.
stream.add_segment("returned")
Parameters
| Parameter | Type | Description |
|---|---|---|
name | str | Name of the new segment column. |
rules | list, optional | CASE-WHEN rules: a list of condition entries plus a final else entry, e.g. [["country", "=", "US", "domestic"], ["international"]]. |
func | callable, optional | A function that accepts the raw pandas DataFrame and returns a collection of segment labels with the same length and order as the eventstream rows. |
sql | str, optional | DuckDB SQL SELECT statement that reads from the eventstream table alias and returns exactly one column — the segment label for each row. Row count and order must match the eventstream. Example: "SELECT CASE WHEN platform = 'mobile' THEN 'mobile' ELSE 'web' END FROM eventstream". |
funnel_events | list of str, optional | Ordered list of at least 2 event names defining a strict, ordered ("closed") funnel. A path is assigned funnel_events[k] if there exists an increasing sequence of event occurrences matching funnel_events[0], funnel_events[1], ..., funnel_events[k] in that order (later steps may be reached via any qualifying occurrence, not necessarily the first or last one — earlier events occurring again after a later step was reached don't un-complete it). A path is assigned the highest such k; if it never completes even funnel_events[0], it is labeled out_of_funnel. Segment values (in ascending funnel order): out_of_funnel, then each event name from funnel_events[0] to funnel_events[-1]. |
time_range | tuple or list, optional | (start, end) — two timestamps (string or pd.Timestamp) bounding an inclusive interval over schema.timestamp_col. Each event is labeled inside if its timestamp falls within [start, end], otherwise outside. |
metric_bins | dict, optional | Split paths into bins by a per-path metric, keyed by metric (required), exactly one of edges / quantiles for the cut points, and optional segment_levels naming the bins. Paths the metric has no value for (time_between is the one that can be undefined — for a path missing either of its two events) get the level "undefined", which is not one of the bins and so is not counted against segment_levels; rename it with rename_segment_levels. |
path_col | str, optional | Path ID column override for funnel_events and metric_bins modes; defaults to schema.path_col. |
Values rules
- Each condition entry is
[column, op, value, label]— translates toWHEN <column> <op> <value> THEN <label>in SQL. A stringvalueis quoted for you, so write"US", not"'US'"— the exception isop="in", whose value is inserted raw and must therefore be a complete SQL tuple:"('GB', 'DE', 'FR')". - The last entry is
[else_label]— the ELSE branch label.