Collapse Events

Merge a run of events into a single representative event.

Two independent decisions. How to chunk the path is exactly one mode: loops or group_col group adjacent rows sharing a value, while event_groups and bounds cut the path into windows, the latter spelled as in split_sessions. How to name the merged event is name, which is orthogonal to the mode.

Breaking on an inactivity gap is not a mode here: run split_sessions(timeout="30m") first and collapse its session column with group_col, which says in two readable steps what one combined argument would have hidden.

Usage

# Collapse any run of the same event
stream.collapse_events(loops=True)

# Collapse only repeated product_view events
stream.collapse_events(loops=["product_view"])

# Merge checkout steps into a single "checkout" event
stream.collapse_events(
    event_groups=["checkout_start", "checkout_step", "checkout_confirm"],
    name="checkout",
)

# One event per session, named after the session's type column
stream.collapse_events(group_col="session_id", name={"col": "session_type"})

# One event per session, named by what the session did
stream.collapse_events(
    group_col="session_id",
    name=[
        {"condition": {"op": "=", "metric": "has_event", "value": True,
                       "metric_args": {"event": "purchase"}},
         "name": "buying_session"},
        "browsing_session",
    ],
)

How it works

Two decisions, and they are independent. Which rows belong together is the mode — one mutually-exclusive argument. What the merged event is called is name, which works the same way whichever mode produced the group. Toy paths for each below.

Choosing the chunks

Every mode below answers one question — which rows belong together — and exactly one of them may be given. loops and group_col group adjacent rows sharing a value: the same event repeating, or the same value of a column. event_groups and bounds cut the path into windows instead, bounds spelled as in Split Sessions.

What the merged event is called is a separate question, answered by name — see Naming the merged event below.

loops

Collapses each self-loop — a run of the same event repeating — into one row, keeping the run's first timestamp. This is the A → A you see on a Transition Graph.

Before: page_view → page_view → page_view → click → click → product_view

stream.collapse_events(loops=True)

After — every run collapses, regardless of which event it is:

page_view → click → product_view

stream.collapse_events(loops=["page_view"])

After — only page_view runs collapse; the two click events are left alone since click isn't in the list:

page_view → click → click → product_view

event_groups

The events that belong to one group: any run drawn from the set collapses into a single event, wherever it occurs — they don't need to be adjacent to each other in the parameter, just adjacent in the path. Events outside the set pass through unchanged and end a run.

This is the difference from loops, which the two names now carry: event_groups=["a", "b"] merges a, a, b, b into one event, loops=["a", "b"] into two.

Before: home → payment_details → shipping_details → review_page → purchase

stream.collapse_events(
    event_groups=["payment_details", "shipping_details", "review_page"],
    name="checkout",
)

After: home → checkout → purchase

bounds

A window is every event between a start_event and the next end_event, inclusive of both.

Before: home → checkout_start → payment_details → checkout_complete → confirmation_page

stream.collapse_events(
    bounds={"start_event": "checkout_start", "end_event": "checkout_complete"},
    name="checkout",
)

After: home → checkout → confirmation_page

group_col

Collapses each run of consecutive rows sharing the same value of a column. A run, not every row carrying the value: a value that comes back later in the path is a second event, not one event stretched across the gap in between.

Before: home → catalog → search → add_to_cart → purchase, with screen_type = browse, browse, browse, action, action

stream.collapse_events(group_col="screen_type")

After — three browse rows collapse into one browse event, and the two action rows collapse into one action event:

browse → action

Sessions are the same shape, since a session id is constant for a run of rows: group_col="session_id" gives one event per session. On its own that names each event after the session id, which is rarely what you want — pair it with name below.

Inactivity is two steps, not a mode

Breaking on a time gap is Split Sessions' job. It writes the boundary into a column, and group_col collapses that column:

stream.split_sessions(timeout="30m").collapse_events(group_col="session_id", name="burst")

Written this way each step says what it does, and the intermediate session column stays available for everything else — a funnel per session, a metric, a diff. A timeout argument on this processor would have hidden the same two operations behind one word.

Naming the merged event

name is orthogonal to every mode above: the mode decides what is merged, name decides what the result is called. It takes three forms.

  • a string — that literal name.
  • {"col": "<column>"} — the value of another column, so a run of sessions can be named by the session's type.
  • a list of cases{"condition": ..., "name": ...} dicts evaluated against the group's own events, optionally closed by a plain string used as the fallback for groups no case matched. A condition is the filter_paths condition tree, over the metrics has_event, event_count, has_all_events, has_any_event, duration, length, time_between, active_days.

A literal

name="checkout" — every group the mode produced becomes an event with that name. Required for the window modes, which have no name of their own to fall back on. loops defaults to the repeated event's name, group_col to the value of the column it grouped on.

Another column's value

Before: home → catalog → add_to_cart → purchase, with session_id = s1, s1, s2, s2 and session_kind = browsing_session, browsing_session, buying_session, buying_session

stream.collapse_events(group_col="session_id", name={"col": "session_kind"})

After — each session becomes a single event named after its type, at the session's first timestamp:

browsing_session → buying_session

Conditions on what happened inside

A list of cases names a group by what its own events were — each condition is a Filter Paths-style condition evaluated against only that group's events. A plain string as the last entry is the fallback for groups no case matched; without one, the mode's own default name is used.

Before — two independent checkout windows, one that reached purchase and one that didn't:

  • user 1: checkout_start → payment_details → purchase → checkout_end
  • user 2: checkout_start → payment_details → checkout_end
stream.collapse_events(
    bounds={"start_event": "checkout_start", "end_event": "checkout_end"},
    name=[
        {
            "condition": {"op": "=", "metric": "has_event", "value": True,
                          "metric_args": {"event": "purchase"}},
            "name": "successful_checkout",
        },
        "abandoned_checkout",  # fallback for groups no case matched
    ],
)

After — the label depends on whether purchase occurred inside each group:

  • user 1: successful_checkout
  • user 2: abandoned_checkout

Because naming is orthogonal, the same list works on any mode. On group_col it is the one-liner for "name each session by what it did", which otherwise takes an add_segment with a windowed SQL query first:

stream.collapse_events(
    group_col="session_id",
    name=[
        {"condition": {"op": "=", "metric": "has_event", "value": True,
                       "metric_args": {"event": "purchase"}},
         "name": "buying_session"},
        "browsing_session",
    ],
)

Collapsing more than once

One call collapses one way. To apply several, chain the calls — every processor returns a new eventstream, and each call sees the output of the one before it, so a later collapse can match an event name an earlier one created:

(
    stream
    .collapse_events(event_groups=["payment_details", "shipping_details"], name="checkout_details")
    .collapse_events(event_groups=["checkout_details", "review_page"], name="checkout")
)

Before: payment_details → shipping_details → review_page → purchase, after: checkout → purchase.

agg

By default, every non-event column takes its first value within the merged rows (ordered by timestamp). agg overrides this per column with one of "first", "last", "min", "max", "mean", "mode", "any".

Before — a price column alongside three consecutive add_to_cart events:

user_ideventtimestampprice
u1add_to_cart00:00:0010
u1add_to_cart00:00:0525
u1add_to_cart00:00:0915
u1purchase00:00:2050
stream.collapse_events(loops=True)  # no agg: price takes the first value
user_ideventtimestampprice
u1add_to_cart00:00:0010
u1purchase00:00:2050
stream.collapse_events(loops=True, agg={"price": "max"})
user_ideventtimestampprice
u1add_to_cart00:00:0025
u1purchase00:00:2050
stream.collapse_events(loops=True, agg={"price": "mean"})
user_ideventtimestampprice
u1add_to_cart00:00:0016.67
u1purchase00:00:2050

Parameters

ParameterTypeDescription
loopsbool or list of str, optionalCollapse each self-loop — a run of the same event repeating — into one row. True collapses every event's loops; a list of event names collapses only those. This is the A → A → A on a transition graph.
event_groupsstr or list of str, optionalEvents that belong to one group: any run drawn from this set collapses into a single event, wherever it occurs in the path. Unlike loops, the run may mix the listed events — event_groups=["a", "b"] turns a, a, b, b into one event, where loops=["a", "b"] turns it into two.
boundsdict, optionalCollapse every event between a start_event and the next end_event, both included. Requires both keys.
group_colstr, optionalCollapse each run of adjacent rows sharing this column's value. Not SQL's GROUP BY: a value that comes back later in the path starts a second event rather than joining the first across the gap. Must differ from the event column — for repeats of the same event use loops.
namestr or dict or list, optionalWhat the merged event is called: a literal string, {"col": "<column>"} to take another column's value, or a list of cases naming each group by what happened inside it. Required for the window modes, which have no natural name of their own; loops defaults to the repeated event's name and group_col to the value of the column being grouped on.
aggdict, optionalAggregation rules for non-event columns when rows are merged, as a {column: agg_func} dict. agg_func is one of "first" (default), "last", "min", "max", "mean", "mode", "any". See agg below. Example: {"price": "max"}.
path_colstr, optionalPath ID column override; defaults to schema.path_col.