retentioneering-tools

retentioneeringApache-2.0活跃维护

用 Python 做点击流和事件日志分析的工具包,支持用户旅程、行为分群、A/B 测试和马尔可夫链模拟,还能让 AI 代理复现并交叉验证分析结果。

Python toolkit, MCP server, and agent skills for reproducible, auditable clickstream and event log analytics. Helps AI agents, data scientists and analysts build, validate, and cross-check product analytics, quantitative UX, customer journeys, graph-based user flows, behavioral segmentation, A/B tests, process mining models, Markov chain simulation

v5.3.0发布于 2026-10-27 16:14 UTC946star +2 / 7 天144fork同步于 2026-11-10 23:52 UTC

怎么上手

由 README 提炼,执行前请确认
pip install retentioneering
硬件要求按 README 说明

README 未说明

下载:源码 zip·GitHub Releases(安装包)本站不镜像安装包

项目说明

由 README 自动翻译提炼

从事件日志分析用户行为路径的 Python 库,帮你定位卡点、对比分组路径并发现行为模式

用 Python 做点击流和事件日志分析的工具包,支持用户旅程、行为分群、A/B 测试和马尔可夫链模拟,还能让 AI 代理复现并交叉验证分析结果。

核心能力

  • 用转移图对比不同分组的路径概率差异
  • 用漏斗按会话统计有序步骤的完成率
  • 用 Step Matrix 和 Step Sankey 对齐关键事件前后步骤
  • 用 filter_paths 按路径模式筛选目标会话
  • 用聚类分析按事件计数等指标划分行为分群

容易踩的坑

需要 Python 3.11 到 3.13;在 notebook 里要用 %pip install retentioneering;自带示例数据需通过 rete.datasets.load_ecom() 加载

内容由模型根据 README 生成,可能不完整或不准确,以官方文档为准。看原文 README

最近发版

5 个版本v5.3.0
  1. v5.3.0最新正式版2026-10-27 · 2026-10-27 16:14 UTC

    Added

    • Path patterns accept the regex quantifier family on gaps: .{m,n}, {n}, {m,}, {,n}, + and ? after . or a class, alongside *. Bounds are inclusive and count the events strictly between the anchors, so add_to_cart->.{0,4}->purchase is "bought within five steps", and cart->[shipping|payment]+->purchase no longer matches an adjacent cart/purchase the way * does. A bounded gap takes no ordinal, so at= and Step Matrix centring are unchanged (#110).
    • Round-bracket groups repeat a sequence: (B->.*->C->.*){2} is unrolled into two copies before matching. {k,} and + are accepted where a .* absorbs the extra repetitions; an upper bound is rejected with the NOT condition that expresses it, and a group whose copies would be glued back to back despite a gap inside ((B->.*->C){2}) is rejected with both readings. Step Sankey lays its blocks out from the unrolled pattern, so each copy gets a block of its own.
    • Condition leaves for 0/1 metrics have a shorthand: {"has_event": "purchase"}, {"matches_pattern": "A->.*->B"}, {"has_any_event": ["A", "B"]}, or {metric: metric_args} for any of has_event, has_any_event, has_all_events, matches_pattern, in_segment. Each means {"op": "=", "metric": ..., "metric_args": ..., "value": True}, and works in filter_paths, in MCP filter_paths steps, and in collapse_events name cases for the 0/1 metrics those support (has_event, has_any_event, has_all_events).
    • method_args of the "kmeans" method accepts n_init ("auto" or a positive int, default "auto") and random_state (int or None, default 42) on all three clustering surfaces (add_clusters, cluster_analysis_data, cluster_analysis). The library used to hard-code scikit-learn's single k-means++ run, so the same features clustered under another seed could land in a different local optimum and there was no way to ask for more initialisations; n_init=10 now does that. The values you pass are carried into best_params, so the saved add_clusters call reproduces the split. Defaults are unchanged.
    • transition_graph(label_scale=...) scales every node and edge label on the canvas (0.5–3, default 1), since the default sizes read too small on slides and in screenshots. HTML exports keep the scale.

    Changed

    • Segment Overview and Cluster Analysis tables group the rows of a bulk metric (event_count_bulk, has_event_bulk, in_segment_bulk) under one collapsible header such as event_count_bulk · mean, and label each row with just its event or segment level, instead of repeating the metric and aggregation on every row.

    Fixed

    • A condition node with no op that is not a shorthand leaf now raises PreprocessingConfigError naming the node, instead of AttributeError: 'NoneType' object has no attribute 'upper'.

    • Cluster Analysis Save/Copy use the features and path column from the displayed analysis even after sidebar edits. Copied code explicitly preserves scaler=None, and clearing a result also clears its saved call.

    • Cluster Analysis records the realized seed for random_state=None in best_params, so saving clusters replays the displayed split. The widget persists its KMeans settings and current seed in state_file; grid selection and restoration reuse that seed, while a new Apply chooses a fresh one.

    • describe()["segments"] reports the events that have no value for a segment column under the <MISSING> sentinel, the same level get_segment_levels lists. Those events used to be dropped, and because share was normalized over the remaining rows it still summed to 1.0 — a segment built with add_segment(func=...) that left 3 of 5 events unassigned reported a 50/50 split instead of 20/20/60, with nothing marking the table as partial. The docstring now states that these counts are over events, not paths ([#188](https://github.com/retentioneering/retentio

  2. v5.2.42026-10-07 · 2026-10-07 13:06 UTC
    查看更新说明

    Changed

    • Links in an MCP report's analysis text now leave a visible mark on Step Matrix and Segment Overview cells. A link used to tint its cell pale yellow for about a second, which on a heatmap was hard to tell apart from the cells around it, so a reader clicking [Tab:event@step] or [Tab:metric@segment] often could not see which number the text meant. The cell now flashes a stronger yellow and keeps an amber outline until the next link into the same widget or a click inside it; a segment column link outlines the whole column. Transition graph links are unchanged

    Fixed

    • Step Sankey now draws anchor= correctly. The data was always right, but the diagram took its layout only from path_pattern, so with an anchor spec the centre block was labelled path_start and no ribbons led into it from the left — step_sankey(anchor="payment_error") looked like a diagram of session starts. The widget now centres the block on the anchored event (at and occurrence included), shows step_window columns on both sides, and for an anchor with an offset, where step 0 is no longer one fixed event, draws step 0 as an ordinary column. path_pattern diagrams are unchanged
    • Transition Graph no longer raises Cannot read properties of null (reading 'isHeadless') during initial rendering when a graph rebuild destroys an instance before its scheduled viewport fit runs. Deferred fit and resize callbacks now check that their graph is still the active, live instance before using it.
    • The MCP server's add_transition_graph and add_step_matrix tools once again tell the agent what the tab shows. The tab itself always rendered correctly, but the summary returned alongside it — the only part of the result the agent reads — came back empty ({"n_events": 0, "top_edges": []}), with and without diff, because it still looked for the widgets' result under a key they had since renamed from event_groups to events. An agent writing a report was therefore interpreting graphs and step matrices it had never seen. Both summaries read the current keys now. A step matrix with a path_pattern has one block per pattern segment, each with its own step axis, and the summary used to cover only the first; it now returns a blocks list with one entry per segment, in pattern order. Tests now run both tools on the bundled ecom dataset and fail on an empty summary, so a future rename of these keys breaks the build instead of silently leaving the agent without data
  3. v5.2.32026-09-25 · 2026-09-25 09:28 UTC
    查看更新说明

    Fixed

    • Transition Graph no longer sits on its "Updating graph..." spinner after "Run all". The graph asked the kernel for its node layout only once it was on screen, and that request waits in the same queue as the cells, so under "Run all" it was answered only after the whole notebook had finished, which in a long notebook meant many minutes of spinners. The layout is now computed together with the graph and arrives with it, so the graph is drawn right away no matter what the kernel is busy with. Running cells one by one looks exactly as before
  4. v5.2.22026-09-17 · 2026-09-17 21:52 UTC
    查看更新说明

    Fixed

    • Installing in Google Colab no longer asks you to restart the session. 5.2.1 raised the ipywidgets floor to >=8.0; Colab ships 7.x and imports it while the kernel starts, so pip install retentioneering upgraded it and pip then required a restart before anything would work — on a fresh notebook, before you had typed a line. The floor is >=7.7 now, which Colab's own version already satisfies, so nothing is upgraded and nothing has to be restarted

    • A correction to what the 5.2.1 entry below claims. It says the old ipywidgets>=7.0 floor meant every widget method failed with ModuleNotFoundError: No module named 'ipython_genutils'. That holds only for 7.6.0 through 7.6.3, which do not list ipython-genutils among their dependencies, so nothing installs it; 7.6.5 and later — including the 7.7.x that Colab ships — do list it, and work. The version that surfaced the failure was chosen by resolving our declared floors at their minimum, which is not a resolution any user gets. So the defect was a floor that admitted those three releases, and >=7.7 is the narrow fix for it; >=8.0 was more than the problem called for and cost Colab users a restart for nothing

  5. v5.2.12026-09-17 · 2026-09-17 20:53 UTC
    查看更新说明

    Changed

    • The MCP server now runs on version 2.x of the MCP Python SDK (mcp[cli]>=2.2,<3). Nothing changes in how you start it or connect to it: rete.mcp.serve(stream, port=8765) takes the same arguments, the server URL is still http://localhost:8765/sse, and existing agent configs keep working. What changed is inside the SDK — its server class was renamed and the transport options moved out of the constructor into run(), so the three lines naming them moved too. The reason to bound the version at all: 5.2.0 asked for mcp[cli]>=1.28.0 with no upper bound, so a fresh install began picking up 2.x on its own the day it was published, which is not a major anyone had tested against. In an environment that already had an HTTP stack installed — Google Colab, or any long-lived environment — that also left pip reporting httpcore 1.0.5 requires h11<0.15, but you have h11 0.16.0: mcp 2.x renamed its HTTP dependencies to httpx2/httpcore2, so the older httpcore was left behind rather than upgraded, while h11, whose name did not change, was bumped for the new one. Nothing was broken in retentioneering itself, but the warning is alarming and the stranded httpcore really is unusable, which can break unrelated libraries in the same notebook. The SDK version is bounded to the tested major now, and httpcore is named as a dependency of its own — it is not imported anywhere, it is there so the resolver upgrades that leftover copy instead of stranding it. pip install retentioneering into a Colab notebook is quiet again, and CI now checks exactly this by installing the package into a deliberately pre-populated environment and running pip check

    • MCP tool calls are serialized. Version 2.x of the SDK runs synchronous tool handlers on worker threads rather than inline on the event loop, so two calls from one agent can now overlap — and every tool writes to the same session state: the active stream, the preprocessors applied to it, and the list of report tabs waiting to be exported. An export_report arriving while an add_transition_graph was still registering its tab could have published a report without that tab, or with a tab computed from a stream another call had just replaced. One tool now runs at a time, which is what 1.x did implicitly by never threading them

    Fixed

    • The package asked for ipywidgets>=7.0, which was never true. With ipywidgets 7.x in the environment, every widget method — transition_graph(), step_matrix(), the rest — failed with ModuleNotFoundError: No module named 'ipython_genutils', because 7.x reaches for a module that no longer ships. Importing the library was fine, since widgets are imported lazily inside the methods, so the floor looked harmless until the moment you asked for a picture. Nothing downstream corrected it either: anywidget itself declares ipywidgets>=7.6.0. The floor is >=8.0 now, which is what the library has actually needed all along — an environment that satisfied only the old floor gets ipywidgets upgraded on the next install instead of a widget layer that cannot import

    • Transition Graph in diff mode felt stuck once a stream had a lot of different events. To give each node its colour, the widget added up both group matrices again for every single node on screen. Those two sums are the same whichever node you ask about, so the work grew with the cube of the number of events. With 600 different events one redraw read about 430 million numbers and took roughly 7 seconds. That redraw is not only the first paint. It runs again every time you move a slider, hide or pin a node, or switch the theme. The sums are now worked out once when the data arrives, in a single walk that also gives the total of the matrix, and asking for one nod

用户点评

登录用户的真实使用记录,可按版本和硬件筛选
共 0 条

    还没有人点评。第一个说出真实体验的人最有价值。

    README 原文

    未翻译

    Retentioneering

    Retentioneering is an open-source Python library for understanding user behavior from event logs. Find where users get stuck, compare the paths different groups take, and discover patterns across clicks, sessions and repeat visits. Explore the data yourself or let an AI agent work with the same tools.

    PyPI Python Downloads License: Apache 2.0 Discord Telegram Try in Colab

    Run a demo notebook in Google Colab – no need to install the library locally, sample data included.

    Quick start · Use cases · Use an AI agent · Docs

    A demo of the interactive features of the transition graph widget. You can highlight a route, compare path groups, focus on the step you want to investigate, and explore incoming and outgoing transition probabilities in the ego view.

    Is it for you?

    Use Retentioneering when you have a question about the sequence of actions like these:

    • A metric changed. Where did journeys change and move the metric?
    • Users stop before reaching a goal. What do they do instead and how do successful paths differ?
    • You want to understand engagement. What behavior types are represented, and how does usage evolve across visits?
    • You need to investigate a flow. Where do checkout, onboarding, support or agent runs loop, branch or stall?

    Bring an event-level export from your analytics platform, warehouse or application logs. All you need is a user, session or case ID; an event name; and a timestamp.

    Quick start

    Python: 3.11 - 3.13.

    Environment: Jupyter (Jupyter Notebook, JupyterLab, JupyterLab Desktop), VS Code, Cursor, Google Colab.

    To install the library, run

    pip install retentioneering
    

    Within a notebook, use %pip install retentioneering instead. See the installation guide for the details.

    To try out the library quickly, without loading your own data, you can use a built-in e-commerce dataset:

    import retentioneering as rete
    
    stream = rete.datasets.load_ecom()  # bundled synthetic store
    stream.transition_graph()
    

    Common use cases

    Below are a few examples of how you can apply retentioneering tools to approach common analytical problems on the bundled e-commerce demo dataset. The code chunks assume stream = rete.datasets.load_ecom().

    Create a binary segment that cuts each path at the boundaries of the affected period: its events inside the period are labelled inside, the rest outside, so one path can contribute to both parts. Then compare user behavior across these parts with the transition graph widget.

    periods = stream.add_segment("period", time_range=("2024-05-19", "2024-06-07"))
    periods.transition_graph(diff=("period", "inside", "outside"))
    

    Click a node (e.g. payment_details) to inspect the routes that pass this node. With this comparison configuration, red means a higher next-step probability inside the period; blue means lower. This locates a behavioral difference to investigate.

    Letting a path contribute to multiple levels of a segment gives more flexibility to diff mode: you can compare not only common attributes like mobile vs. desktop, acquisition channels, or experiment groups, but dynamic features as well: first session vs. the others, weekends vs. weekdays, etc.

    Count sessions that complete the steps in order – rather than whole user journeys (path_col="session_id") – with the funnel widget:

    steps = ["cart", "shipping_details", "purchase"]
    stream.funnel(steps=steps, path_col="session_id")
    

    Then compare sessions that stopped at the shipping stage of this funnel with those that completed it using the step matrix widget in diff mode:

    (
        stream
            .add_segment("stage", funnel_events=steps, path_col="session_id")
            .step_matrix(
                path_pattern="cart->.*->shipping_details",
                step_window=2, path_col="session_id",
                diff=("stage", "shipping_details", "purchase"),
            )
    )
    

    The path_pattern="cart->.*->shipping_details" argument of the step matrix breaks down the diagram into two parts: around cart and around shipping_details. You can inspect these surroundings and compare sessions that reached shipping_details but not purchase with those that completed the funnel.

    Instead of using traditional tree-like diagrams for path exploration, you can use Step Sankey, which shows the same numbers as a Step Matrix, drawn as flows. In this example, we align all the paths by the payment_error event and display the two steps on either side of it:

    stream.step_sankey(
        anchor="payment_error", step_window=2,
        path_col="session_id",
    )
    

    A plain event name anchors each path on its first occurrence. An anchor spec chooses the position more precisely: which occurrence to use, which event of a pattern to center on (at), or how far to shift from it (offset). For example, align sessions on their last payment error to see whether users recover after it or give up:

    stream.step_sankey(
        anchor={"pattern": "payment_error", "occurrence": "last"},
        step_window=2, path_col="session_id",
    )
    

    After the last error, sessions end more often than after the first one: two steps later, path_end takes 41% instead of 32%. Either way, the next step is almost always support_chat (89%).

    Find sessions that opened the cart, then ended without reaching shipping or support:

    abandoned = stream.filter_paths(
        {"metric": "matches_pattern", "op": "=", "value": True,
         "metric_args": {
             "pattern": "cart->[^shipping_details|support_chat]*->path_end"
         }},
        path_col="session_id",
    )
    abandoned.step_sankey(path_pattern="cart", path_col="session_id", step_window=3)
    

    The filter_paths data processor can filter paths according to a path metric value, such as length, duration, event count, etc. In our case we use the matches_pattern metric that checks if a path matches the regex-like pattern cart->[^shipping_details|support_chat]*->path_end.

    To explore clusters interactively, start with a bare call of the Cluster Analysis widget and set everything up in its sidebar:

    stream.cluster_analysis()
    

    Here users are clustered by event_count_bulk, which expands into one count per event type, over a grid of 3 to 8 clusters scored by the silhouette metric. The heatmap compares mean metric values across clusters (blue for lower values, red for higher). Overview metrics such as length or in_segment_bulk describe the clusters without changing the features used for clustering. Pick another partition on the Silhouette tab if you are not satisfied with the silhouette-best split. Once you find an optimal split, label the clusters in the header, click Save Clusters and save them as a segment.

    The same configuration can also be passed directly:

    stream.cluster_analysis(
        features=[{"metric": "event_count_bulk"}],
        method_args={"n_clusters": "3-8"},
        overview_metrics=[
            {"metric": "event_count_bulk"},
            {"metric": "length"},
            {"metric": "in_segment_bulk", "metric_args": {"segment_name": "acquisition_channel"}},
        ],
    )
    

    And the partition saved in the GIF above – four clusters, renamed – becomes a segment column with add_clusters and rename_segment_levels:

    user_types = (
        stream
            .add_clusters(
                "user_type", features=[{"metric": "event_count_bulk"}],
                method_args={"n_clusters": 4},
            )
            .rename_segment_levels("user_type", {
                "cluster_0": "browsers",
                "cluster_1": "researchers",
                "cluster_2": "buyers",
                "cluster_3": "light_users",
            })
    )
    

    Once clusters are saved as a segment, explore them like any other segment: compare them in the diff mode of any widget or side by side in Segment Overview.

    Cluster sessions according to behavioral types like this:

    stream.cluster_analysis(
        features=[{"metric": "event_count_bulk"}],
        method_args={"n_clusters": "4-8"},
        path_col="session_id"
    )
    

    Suppose 4 clusters look best. add_clusters reproduces that split as the session_type segment – the same result as Save Clusters in the widget. Then we can collapse sessions and treat them as single events with the collapse_events data processor and visualize the session flow with any widget, such as the transition graph:

    visits = (
        stream
            .add_clusters(
                "session_type", features=[{"metric": "event_count_bulk"}],
                method_args={"n_clusters": 4}, path_col="session_id",
            )
            .collapse_events(group_col="session_id", name={"col": "session_type"})
            .rename_events({
                "cluster_0": "browsing",
                "cluster_1": "quick_visit",
                "cluster_2": "purchase_visit",
                "cluster_3": "promo_visit"
            })
    )
    visits.transition_graph()
    

    See more analysis recipes in the docs.

    Bring your own data

    The minimum input is a table with three columns – user_id, event and timestamp:

    user_ideventtimestamp
    user_1signup2026-01-01 10:00:00
    user_1project_created2026-01-01 10:02:00
    user_1teammate_invited2026-01-01 10:05:00

    Retentioneering uses the Eventstream class to store data and to provide access to all the library's methods.

    import pandas as pd
    import retentioneering as rete
    
    events = pd.read_csv("events.csv", parse_dates=["timestamp"])
    stream = rete.Eventstream(events)
    stream.transition_graph()
    

    Use a schema to map other column names, declare path segments or pre-existing sessions. Any dataset readable by pandas – such as CSV, Parquet or the result of a warehouse query – can become an eventstream.

    These are starting points for common exports; check identity and timestamp fields in your actual data.

    SourcePath IDEventTimestamp
    GA4 BigQueryuser_pseudo_idevent_nameevent_timestamp (microseconds)
    Amplitudeuser_id or amplitude_idevent_typeevent_time
    Mixpanelproperties.distinct_ideventproperties.time (Unix seconds)
    Segment tracksuser_id or anonymous_ideventtimestamp

    Flatten nested properties and convert numeric timestamps to datetimes with the correct unit before loading. For example, after exporting the three GA4 fields above to a DataFrame named ga4:

    ga4["event_timestamp"] = pd.to_datetime(ga4["event_timestamp"], unit="us")
    stream = rete.Eventstream(ga4, schema={
        "path_cols": ["user_pseudo_id"], "event_col": "event_name",
        "timestamp_col": "event_timestamp",
    })
    

    Common analysis workflow

    • Prepare paths. Data processors filter events or whole journeys, split sessions, collapse repeated actions and define segments. Each returns a new eventstream, so steps chain into a pipeline.
    • Explore. Interactive widgets show paths as graphs, step matrices, Sankey diagrams, funnels and clusters, and compare any two groups.
    • Build your own outputs. Every widget has a headless *_data() twin that returns the numbers behind the chart, for custom charts, reports and checks.
    • Share. export_html() saves a widget as a standalone interactive page that opens without Python.

    Explore agent runs, conversations and other event sequences

    A path doesn't have to be a user journey. Treat each agent run as a path and its internal steps – tool calls, retries, errors, validations – as events, then use the same tools to see where runs loop, recover or fail, and how successful runs differ from failed ones. See the agent-run example notebook (open in Colab) for a synthetic walkthrough.

    Paths of synthetic agent runs, comparing failed runs with successful ones. Red emphasizes the repeated tool:error and retry cycle.

    The same approach works for other processes: decide what one path is and which steps become its events. For example:

    • Support conversations: a path is the sequence of message intents and handoffs within one conversation, e.g. billing_question → bot_answer → rephrased_question → escalation → agent_answer → resolved.
    • Tickets: a path is the sequence of a ticket's status changes and reassignments, e.g. opened → triaged → assigned:tier1 → waiting_for_customer → reassigned:tier2 → closed.
    • Learning sessions: a path is the sequence of lessons, attempts and their results within a study session or across a learner's whole course, e.g. lesson_3 → quiz_3:fail → lesson_3_review → quiz_3:pass → lesson_4.

    Work with an AI agent

    Give your coding agent the product-analytics skill and an event file. The skill guides it through inspecting the log, choosing a recipe, running the analysis and checking the result. See skill setup for supported workflows.

    For example:

    Use Retentioneering to investigate where sessions in events.csv stall between cart and purchase. Check the schema and event order first. Compare sessions that purchased with those that stopped, keep non-purchasing sessions, and show the counts behind the main differences. Return the runnable analysis and an interactive HTML report.

    Prefer an MCP connection? The data MCP server (beta) exposes an Eventstream to a compatible agent from a local Python environment. Its report collects several tools in tabs and links every number in the text to the element it comes from – a graph edge, a step matrix cell or a segment overview cell:

    • Read the current API index, full text docs or documentation MCP guide. Documentation access supplies reference material; data analysis also needs Python execution and access to the event file.
    • This README uses 5.x. Start with import retentioneering as rete and rete.Eventstream(df, schema={...}). Older 2.x/3.x examples use a different API.
    • Choose and declare the path grain (path_cols); pass path_col to analyses when switching between users and sessions. Declare comparison columns in segment_cols.
    • Use transition_graph, step_matrix, step_sankey, funnel, segment_overview and cluster_analysis. Their *_data() methods return computed results for inspection and automation; return types vary by tool.
    • Graph, matrix, Sankey and funnel comparisons take diff=("segment", "A", "B") and show A minus B. Check group sizes, metric definitions and the corresponding data output.
    • Processors return a new Eventstream. Assign the result, preserve the intended event order and population, and validate a complete sequence with a path pattern rather than inferring it from separate graph edges.
    • Save and replay transformations with Eventstream.recipe(). Keep findings separate from hypotheses about their causes.

    Telemetry

    Anonymous usage telemetry is enabled by default. Sensitive data like event names, identifiers, path contents and parameter values is never sent; see what is collected. Set RETENTIONEERING_NO_TRACK=1 before starting Python to disable it.

    Documentation and community

    Quick start · Eventstream · Widgets · Data processors · Analysis recipes

    Using a 3.x example? Version 5 rewrites the API. See the migration guide; legacy Sequences, Cohorts, StatTests and the visual Preprocessing Graph remain on the 3.x branch.

    Bring a question, a useful recipe or a small reproducible example to GitHub issues, Discord or Telegram. Contributions to methods, widgets, examples and agent workflows are welcome – see CONTRIBUTING.md.

    Apps are better with math. Join us!

    Citing Retentioneering

    If you use Retentioneering in your research, please cite it:

    @software{retentioneering,
      title   = {Retentioneering: understanding user behavior from event logs},
      author  = {Godzi, Maxim and Kukushkin, Vladimir and Zaytsev, Anatoly},
      year    = {2026},
      url     = {https://github.com/retentioneering/retentioneering-tools},
      license = {Apache-2.0},
    }
    

    License and commercial model

    Retentioneering-tools is open-source software licensed under the Apache License, Version 2.0.

    Retentioneering is a community research laboratory dedicated to developing new analytics methodology and open-source tools.

    Copyright retentioneering-tools v.5.0 Maxim Godzi, Vladimir Kukushkin and Anatoly Zaytsev. Updates may include software developed by the Retentioneering community.

    You are free to use, modify, distribute, and build commercial products with Retentioneering-tools, subject to the terms of the Apache-2.0 license.

    Other Retentioneering libraries, packages and managed execution services, enterprise integrations, premium diagnostic workflows, and hosted collaboration features are separate proprietary products and are governed by their respective commercial terms. Additional details are provided in COMMERCIAL.md.

    The Apache-2.0 license applies only to the source code and assets distributed in this repository. It does not grant rights to use the Retentioneering name, logo, trademarks, hosted services, proprietary cloud infrastructure, or commercial content that is not distributed in this repository.

    We welcome contributions from individuals and organizations. Contributions to Retentioneering-tools are accepted under the contribution terms described in CONTRIBUTING.md.

    Our goal is to keep the core analytical language and ecosystem open, extensible, and useful for independent analysts, researchers, startups, and enterprise teams, while funding long-term maintenance through optional commercial products and services.