Migrating from 3.x
retentioneering 5.0 is a ground-up rewrite, not an incremental release. If you are following a tutorial, a blog post, or a notebook written for 3.3.0, most of the code in it will not run as-is. This page maps the old API onto the new one so you can tell, method by method, whether something was renamed, replaced, or dropped.
If you are new to the library, you can skip this page entirely — start with the Quick Start.
What changed, in one paragraph
The pandas-based engine was replaced with a DuckDB-backed one, so the same analyses run at production-log scale on a laptop. The old iframe + CDN-loaded widgets were replaced with anywidget-based ones whose JavaScript is open source, lives in this repository, and ships inside the wheel — nothing is downloaded at runtime, and widgets now work in VS Code and Cursor as well as Jupyter. The data_processor / preprocessor / params_model machinery is gone: data processors are now plain methods with plain keyword arguments. And two things are new with no 3.x counterpart — an MCP server that lets an LLM agent drive the analysis, and the Segment Overview widget.
CHANGELOG.md in the repository holds the exhaustive, itemized delta under [5.0.0].
Should you upgrade?
Upgrade if you want the new engine, the new widgets, agent support, or ongoing fixes. Stay on 3.x if your work depends on the Preprocessing Graph, Cohorts, StatTests, or Sequences — none of them exist in 5.x yet (see What is gone below).
The 3.x engine is preserved on the 3.x branch for reference and patches:
pip install "retentioneering<4" # the 3.x line
pip install -U retentioneering # 5.x
The two versions install under the same package name, so they cannot coexist in one environment — use separate virtualenvs if you need both.
Concepts that shifted
Four changes explain most of the surprises when porting old code.
Paths, not users. 3.x was built around users. 5.x is built around paths, and a path is whatever grain you declare: path_col="user_id" reproduces the old behavior, path_col="session_id" switches the same eventstream to session grain without rebuilding it. See Key concepts.
Everything is immutable. Every data processor returns a new Eventstream and never modifies the one it was called on. Code written against 3.x's in-place semantics needs to capture the result:
stream = stream.filter_events(drop={"event": ["checkout_bug"]}) # not just stream.filter_events(...)
This is also why copy() is gone — there is nothing to protect against.
Segments replace ad-hoc grouping. A segment column describes a whole split at once (US / DE / FR), not one group, and its values live per event — so a segment can change along a path. Segments drive comparison everywhere: every core widget takes diff=(segment_col, value1, value2). See Segments.
Every widget has a headless twin. stream.step_matrix() renders; stream.step_matrix_data() returns the DataFrame behind it, with the same data arguments. If you used 3.x tools to get numbers rather than pictures, the *_data() methods are what you want. See Headless mode.
Renamed or re-signatured
Same concept, different call shape.
| 3.3.0 | 5.x | Note |
|---|---|---|
to_dataframe(copy=False) | to_dataframe(exclude_start_end=True) | The result is always a snapshot, so there is no copy flag. |
filter_events(func) | filter_events(keep=, drop=, func=, sql=) | func is now one of four alternative modes; keep/drop cover the common cases without a lambda. |
rename(rules: list[dict]) | rename_events(mapping: dict) | A plain {old: new} dict. |
collapse_loops(suffix, time_agg) | collapse_events(consecutive=, event_groups=, ...) | Loop-squashing is now one mode of a general merging processor. |
group_events / group_events_bulk | collapse_events(event_groups=...) or rename_events | Renaming several events to one name is rename_events; merging runs of them is collapse_events. |
split_sessions(timeout, delimiter_events, ...) | split_sessions(timeout=, separator=, start_event=, end_event=, ...) | timeout now needs an explicit unit — "30m" or a pd.Timedelta. Bare numbers are rejected. |
truncate_paths(drop_before, drop_after, ...) | truncate_paths(start_event, end_event) | Window anchors are the start_event/end_event pair everywhere in 5.x. |
drop_paths() | filter_paths(condition) | A condition tree over path metrics — {"op": ">", "metric": "length", "value": 5} — instead of fixed thresholds. |
add_positive_events / add_negative_events | add_events(name, source_events=[...]) | One processor for synthetic events; the positive/negative distinction was only naming. |
label_new_users / label_lost_users / label_cropped_paths | add_segment, add_events(churn=...), to_daily_states | Labelling is a segment; a churn marker is a synthetic event; lifecycle states are their own processor. |
clusters() (stateful fit()/extract_features()) | cluster_analysis() + add_clusters() | Explore interactively, then persist the split as a segment column. |
transition_matrix() | transition_graph_data() | Same matrix, now the headless twin of the graph. |
describe() / describe_events() | describe() | One dict: schema, shape, date range, event frequency, path length/duration stats. |
add_custom_col() / index_events() | schema={"custom_cols": [...]} | Extra columns ride along automatically; declare them only if you want strict control. |
append_eventstream() | pd.concat(...) before constructing | Combine the frames, then build one Eventstream. |
pipe() | plain chaining, or recipe() | Processors chain directly; recipe()/from_recipe() replay a pipeline elsewhere. |
Widget classes were reimplemented from scratch, but kept their names and are reached the same way — as methods on the eventstream: transition_graph, step_matrix, step_sankey, funnel, and cluster_analysis (3.x's Clusters).
What is gone
No equivalent in 5.x today. These were cut deliberately to get the rewrite shipped, and may return — requests and pull requests are welcome on the issue tracker.
| Gone | Closest thing available now |
|---|---|
| Preprocessing Graph (visual no-code pipeline builder) | Chained data processors in code; for agents, the MCP server's preprocessor lists. |
| Cohorts | add_segment over a signup-period column plus Segment Overview; to_daily_states for lifecycle-state retention. |
| StatTests | Nothing built in — pull per-path values with get_metrics() and run your own test in scipy. |
| Sequences | The matches_pattern path metric and path_pattern on Step Matrix / Step Sankey answer pattern questions, but there is no n-gram frequency table. |
timedelta_hist, user_lifetime_hist, event_timestamp_hist | get_metrics() returns duration, time_between, first_event_time and friends per path — plot them with your own matplotlib/plotly call. describe() gives the percentiles directly. |
Naming conventions
5.x follows one vocabulary throughout (ADR-0008), which is worth skimming before you port a pipeline:
- Column arguments are always
path_col,event_col,timestamp_col,session_col,segment_col. - Window anchors are always the
start_event/end_eventpair. - Durations are strings with an explicit unit (
"30m") or apd.Timedelta; time outputs are seconds. <REST>is the diff sentinel for "every other value of this segment".- Data processors are verb-first (
filter_events), widgets are nouns (funnel), headless twins are<widget>_data.
Requirements
5.x requires Python 3.10 or later (3.3.0 supported 3.8–3.11). Widgets render in Jupyter, JupyterLab, VS Code, Cursor, and Google Colab — see Installation for the one JupyterLab caveat.
Next steps
- Quick Start — the 5.x pipeline end to end in five minutes.
- Path Analysis — what each visualization actually computes, and when to trust it.
- Eventstream — schema, path grain, and the methods that aren't processors or widgets.