Cluster Analysis
An interactive tool for finding an optimal splitting of paths by behavioral metrics. Allows you to inspect clusters in a Segment Overview-style heatmap and offers the best possible splitting from the silhouette score perspective. Once the splitting looks right, you can label the clusters and save them as a new segment column of the eventstream right from the UI by clicking "Save Clusters".
How it works
Every representation in Path Analysis summarizes paths by their shape — which events, in which order. Clustering goes the other way: it describes each path by a handful of numbers, then groups paths whose numbers look alike. Two users who never visited the same screen can still land in the same cluster if they were equally brief, equally repetitive, or equally unlikely to buy.
The pipeline is three steps, and each maps to one argument:
- Describe.
featuresturns every path into a row of numbers, using the shared path metrics registry —length,duration,active_days, a per-event count, whether a pattern matched. This choice is the analysis: clusters can only differ along dimensions you measured. - Scale.
scalerputs those numbers on a comparable footing. Without it adurationin seconds (six digits) drowns out alengthin events (two digits), and the clustering silently becomes "group by duration". Hence the"minmax"default. - Split.
n_clustersfixes the number of groups, or names a range to search. Given a range, the widget clusters at every size in it and keeps the one with the best silhouette score — a measure of how much tighter paths sit within their own cluster than to the nearest other one. It is a hint about structure, not a verdict: a mediocre score across the whole range usually means the paths form a continuum rather than distinct groups.
An optional fourth knob, NMF, sits between steps 2 and 3.
What comes back is not the clusters themselves but a way to read them: the
overview heatmap puts overview_metrics in rows and clusters in columns,
exactly like Segment Overview, so you can see
what actually distinguishes cluster 2 from cluster 3 and give it a name. Note
that features and overview_metrics are independent on purpose — profiling
clusters on metrics you did not cluster by is how you find out whether the
split means anything beyond its own inputs.
Clusters are not a special kind of object. Once a split looks right, "Save
Clusters" (or add_clusters) writes the
labels into the eventstream as an ordinary segment column, and
from there every segment-aware tool — diff mode, Segment Overview, the
in_segment metric — works on it unchanged.
Choosing a cluster count is a judgment call, not a computation. Prefer the smallest number of clusters you can still describe in words; a split you cannot name is a split you cannot act on.
Usage
stream.cluster_analysis(
features=[{"metric": "length"}, {"metric": "duration"}, {"metric": "event_count_bulk"}],
n_clusters="3-6",
)
Examples
Basic
stream.cluster_analysis(
features=[
{"metric": "event_count_bulk", "metric_args": {"events": ["catalog", "product_view", "add_to_cart", "purchase"]}},
],
n_clusters=3,
)
Parameters
Data
Data parameters change the computed result. They are the arguments of the
widget's headless twin stream.cluster_analysis_data() — see
headless mode below — with three exceptions, all of which the
twin accepts and this constructor does not:
nmf_components— see NMF below. It is not missing from the widget: the sidebar has an NMF toggle and a component-count field, and the result arrives as the H-matrix / W Cluster Means tabs. It is only unavailable as a constructor argument, so set it in the sidebar or use the headless twin.min_cluster_size,cluster_selection_epsilon— tuning knobs formethod="hdbscan". The method itself can be passed to the widget, but there is no sidebar control to switch it and no way to tune these two from the widget at all. Use the headless twin for hdbscan work.
| Parameter | Type | Description |
|---|---|---|
features | list of dict, optional | Metric configurations used as clustering features (see the Path Metrics). If omitted, the sidebar starts pre-filled with a wildcard event_count_bulk metric (one column per event in the eventstream) — that pre-fill is a starting point to edit, not something that runs on its own: passing features explicitly (or clicking "Apply" in the sidebar) is what actually triggers clustering. |
method | {"kmeans", "hdbscan"}, default "kmeans" | Clustering algorithm. |
scaler | {"minmax", "std"}, optional | Feature scaler applied before clustering; default "minmax". |
n_clusters | int, list of int, or str, optional | Number of clusters. A single int fixes the cluster count; a list of ints or a range string (e.g. "3-8") runs a silhouette-scored grid search over that range and picks the best. Defaults to "3-8". |
overview_metrics | list of dict, optional | Metrics shown in the overview heatmap after clustering (independent of features). If omitted, the sidebar starts pre-filled with a wildcard event_count_bulk metric here too (mean count per event); same as features, it only takes effect once you click "Apply" or pass the argument explicitly. Both features and overview_metrics accept metric configs from the same Path Metrics registry. |
path_col | str, optional | Path ID column override; defaults to schema.path_col. |
Display
Display parameters only affect how the widget is rendered.
| Parameter | Type | Description |
|---|---|---|
height | int, default 520 | Widget height in pixels. |
sidebar_open | bool, default True | Whether the sidebar starts open. |
state_file | str, optional | JSON file the widget state is bound to; see Saving widget state. |
NMF
Turning on NMF in the sidebar (or passing nmf_components to the headless
twin) inserts a dimensionality-reduction step between scaling and clustering:
the feature matrix is factorized into the requested number of non-negative
components, and paths are clustered on those components instead of on the raw
metrics. It is independent of the clustering algorithm — the usual pairing is
NMF with k-means.
Reach for it when features is wide and correlated — a wildcard
event_count_bulk over a few dozen events is the standard case. Twenty
near-duplicate count columns let the loudest events dominate the distance
calculation; a handful of components spreads the signal out. Like n_clusters,
nmf_components accepts a single number or a range to grid-search.
The payoff is interpretability, and it arrives as two extra tabs:
- H-matrix — how each component is built out of your features. This is what
names a component: one loading heavily on
searchandfilter_resultsis "browsing intensity", whatever the algorithm called it. - W Cluster Means — how strongly each cluster expresses each component. Read it together with the H-matrix and each cluster gets a description in terms of behavior rather than a number.
The cost is that clusters no longer sit in the space of your original metrics,
so overview_metrics (which always report raw per-path values) stays the honest
check on whether the split means anything.
From the widget to a segment column
The widget explores; add_clusters
persists. The two differ in one place: cluster_analysis searches a range of
cluster counts ("3-8" by default) and picks a winner, while add_clusters
materializes one specific clustering and therefore needs an exact
n_clusters.
You don't have to transcribe it. "Save Clusters" in the sidebar writes the
matching add_clusters(...) call — optionally with your renamed cluster labels —
and headlessly the same value comes back as best_params:
features = [{"metric": "length"}, {"metric": "active_days"}]
result = stream.cluster_analysis_data(features=features)
result["best_params"] # e.g. {"n_clusters": 3}
stream = stream.add_clusters("behavior", features=features, **result["best_params"])
best_params holds only the parameters that were searched over, so pass the
same features alongside it.
Headless mode
stream.cluster_analysis_data()
Run cluster analysis headlessly and return a dict of results.
Pass lists for n_clusters / nmf_components / min_cluster_size to trigger
grid search with silhouette scoring. For the kmeans method (the default),
n_clusters defaults to "3-8" if omitted — including for
nmf_components-only searches.
best_params holds the concrete parameter values actually used to produce
overview_df (the winning combination when searching, or just the fixed
values passed in otherwise) — pass it straight to add_clusters to
materialize the same clustering as a segment column.