Funnel Queries¶
Build typed funnel conversion analysis against Mixpanel's Insights engine — define steps, exclusions, and conversion windows inline without creating saved funnels first.
New in v0.3
Workspace.query_funnel() is the typed way to run funnel analysis programmatically. It supports capabilities not available through the legacy funnel() method, including ad-hoc step definitions, per-step filters, exclusions, holding constant properties, and session-based conversion.
When to Use query_funnel()¶
query_funnel() builds funnel bookmark params and posts them to the Insights engine. The legacy funnel() method queries pre-saved funnels by ID. Use query_funnel() when you need any of the capabilities in the right column:
| Capability | Legacy funnel() |
query_funnel() |
|---|---|---|
| Query a saved funnel by ID | funnel(funnel_id=123) |
Use query_saved_report() |
| Define steps inline | Not available | ["Signup", "Purchase"] |
| Per-step filters | Not available | FunnelStep("Purchase", filters=[...]) |
| Per-step labels | Not available | FunnelStep("Purchase", label="High-Value") |
| Exclusions between steps | Not available | exclusions=["Logout"] |
| Hold properties constant | Not available | holding_constant=["platform"] |
| Conversion window control | Not available | conversion_window=7, conversion_window_unit="day" |
| Session-based conversion | Not available | conversion_window_unit="session" |
| Step ordering modes | Not available | order="any" |
| Property breakdowns | on="country" |
group_by="country" |
| Save query as a report | N/A | result.params → create_bookmark() |
Use the legacy funnel() when:
- You have an existing saved funnel in Mixpanel and just need its results →
funnel(funnel_id=123) - You need simple segmentation of a saved funnel →
funnel(funnel_id=123, on="country")
Getting Started¶
The simplest possible funnel — two steps with default settings (14-day conversion window, last 30 days):
import mixpanel_data as mp
ws = mp.Workspace()
result = ws.query_funnel(["Signup", "Purchase"])
print(f"Conversion: {result.overall_conversion_rate:.1%}")
# Conversion: 12.3%
print(result.df)
# step event count step_conv_ratio overall_conv_ratio avg_time avg_time_from_start
# 1 1 Signup 1000 1.00 1.00 0.0 0.0
# 2 2 Purchase 123 0.12 0.12 3600.0 3600.0
Add a conversion window and time range:
# 7-day conversion window over the last 90 days
result = ws.query_funnel(
["Signup", "Add to Cart", "Checkout", "Purchase"],
conversion_window=7,
last=90,
)
Steps¶
Plain Strings¶
The simplest way to define steps — pass event names as strings:
At least 2 steps are required, up to a maximum of 100.
The FunnelStep Class¶
For per-step configuration, use FunnelStep objects:
from mixpanel_data import FunnelStep, Filter
result = ws.query_funnel([
FunnelStep("Signup"),
FunnelStep(
"Purchase",
label="High-Value Purchase",
filters=[Filter.greater_than("amount", 50)],
),
])
FunnelStep fields:
| Field | Type | Default | Description |
|---|---|---|---|
event |
str |
(required) | Mixpanel event name |
label |
str \| None |
None |
Display label (defaults to event name) |
filters |
list[Filter] \| None |
None |
Per-step filter conditions |
filters_combinator |
"all" \| "any" |
"all" |
How per-step filters combine (AND/OR) |
order |
"loose" \| "any" \| None |
None |
Per-step ordering override |
Plain strings and FunnelStep objects can be mixed freely:
result = ws.query_funnel([
"Signup", # plain string — no filters needed
FunnelStep("Purchase", filters=[Filter.greater_than("amount", 50)]),
])
Per-Step Filters¶
Apply filters to individual steps using FunnelStep.filters. These restrict which events count for that specific step:
from mixpanel_data import FunnelStep, Filter
result = ws.query_funnel([
FunnelStep("Signup", filters=[Filter.equals("source", "organic")]),
FunnelStep("Purchase", filters=[
Filter.equals("country", "US"),
Filter.greater_than("amount", 25),
]),
])
By default, multiple per-step filters combine with AND logic. Use filters_combinator="any" for OR logic:
result = ws.query_funnel([
"Signup",
FunnelStep(
"Purchase",
filters=[
Filter.equals("country", "US"),
Filter.equals("country", "CA"),
],
filters_combinator="any", # match US OR CA
),
])
See Insights Queries — Filters for the full list of Filter factory methods.
Conversion Window¶
Control how long users have to complete the funnel:
# 7 days (default unit is "day")
result = ws.query_funnel(["Signup", "Purchase"], conversion_window=7)
# 2 hours
result = ws.query_funnel(["View", "Click"], conversion_window=2, conversion_window_unit="hour")
# 4 weeks
result = ws.query_funnel(["Signup", "Purchase"], conversion_window=4, conversion_window_unit="week")
Conversion Window Units¶
| Unit | Max value | Description |
|---|---|---|
"second" |
31,708,800 | Seconds (min: 2) |
"minute" |
528,480 | Minutes |
"hour" |
8,808 | Hours |
"day" (default) |
367 | Days |
"week" |
52 | Weeks |
"month" |
12 | Months |
"session" |
12 | Sessions |
All maximum values correspond to approximately 366 days.
Session-Based Conversion¶
Use conversion_window_unit="session" to count conversions within a single Mixpanel session:
# Users must complete all steps within one session
result = ws.query_funnel(
["View Product", "Add to Cart", "Purchase"],
conversion_window=1,
conversion_window_unit="session",
math="conversion_rate_session",
)
Note
Session-based conversion requires conversion_window=1 and math="conversion_rate_session".
Ordering¶
The order parameter controls how steps must be completed:
| Order | Behavior |
|---|---|
"loose" (default) |
Steps must happen in the specified order, but other events can occur between them |
"any" |
Steps can happen in any order within the conversion window |
# Steps in any order
result = ws.query_funnel(
["Feature A Used", "Feature B Used", "Feature C Used"],
order="any",
conversion_window=30,
)
Per-Step Order Override¶
When the top-level order is "any", individual steps can override to "loose":
result = ws.query_funnel(
[
FunnelStep("Signup"), # must come first (loose)
FunnelStep("Feature A", order="any"),
FunnelStep("Feature B", order="any"),
],
order="any",
)
Aggregation¶
The math parameter controls what metric is computed. Default: "conversion_rate_unique".
Conversion Rates¶
| Math type | What it measures |
|---|---|
"conversion_rate_unique" (default) |
Unique-user conversion rate |
"conversion_rate_total" |
Total-event conversion rate |
"conversion_rate_session" |
Session-based conversion rate |
# Total-event conversion (counts all events, not just unique users)
result = ws.query_funnel(
["View", "Purchase"],
math="conversion_rate_total",
)
Raw Counts¶
| Math type | What it counts |
|---|---|
"unique" |
Unique users per step |
"total" |
Total event count per step |
# Raw unique user counts at each step
result = ws.query_funnel(["Signup", "Purchase"], math="unique")
Property Aggregation¶
| Math type | Aggregation |
|---|---|
"average" |
Mean of a numeric property per step |
"median" |
Median value |
"min" / "max" |
Extremes |
"p25" / "p75" / "p90" / "p99" |
Percentiles |
# Average purchase amount at each step
result = ws.query_funnel(
["Signup", "Purchase"],
math="average",
math_property="amount",
)
Filters¶
Global Filters¶
Apply filters across all steps with where=:
from mixpanel_data import Filter
# Filter the entire funnel
result = ws.query_funnel(
["Signup", "Purchase"],
where=Filter.equals("country", "US"),
)
# Multiple global filters (AND logic)
result = ws.query_funnel(
["Signup", "Purchase"],
where=[
Filter.equals("platform", "web"),
Filter.is_true("is_premium"),
],
)
Global filters apply to all steps in the funnel. For step-specific filtering, use FunnelStep.filters (see Steps — Per-Step Filters).
See Insights Queries — Available Filter Methods for the complete filter reference.
Cohort Filters¶
Restrict the funnel to users in a cohort — saved or inline:
from mixpanel_data import Filter, CohortCriteria, CohortDefinition
# Saved cohort
result = ws.query_funnel(
["Signup", "Purchase"],
where=Filter.in_cohort(123, "Power Users"),
)
# Inline cohort — no pre-saved cohort needed
active_users = CohortDefinition(
CohortCriteria.did_event("Login", at_least=5, within_days=7)
)
result = ws.query_funnel(
["Signup", "Purchase"],
where=Filter.in_cohort(active_users, name="Active Users"),
)
See Insights Queries — Cohort Filters for the full cohort filter reference.
Custom Property Filters¶
Use saved or inline custom properties in funnel filters:
from mixpanel_data import Filter, CustomPropertyRef
result = ws.query_funnel(
["Signup", "Purchase"],
where=Filter.greater_than(property=CustomPropertyRef(42), value=100),
)
Warning
Custom property filters in funnel where= may cause server errors due to a known Mixpanel API bug. Custom property breakdowns and measurement work reliably.
See Insights Queries — Custom Properties in Queries for InlineCustomProperty, validation rules, and full options.
Breakdowns¶
Break down funnel results by property values with group_by:
from mixpanel_data import GroupBy
# Simple string breakdown
result = ws.query_funnel(["Signup", "Purchase"], group_by="platform")
# Multiple breakdowns
result = ws.query_funnel(["Signup", "Purchase"], group_by=["country", "platform"])
# Numeric bucketing
result = ws.query_funnel(
["Signup", "Purchase"],
group_by=GroupBy("amount", property_type="number", bucket_size=50),
)
Cohort Breakdowns¶
Segment funnel results by cohort membership:
from mixpanel_data import CohortBreakdown
# Compare power users vs. everyone else through the funnel
result = ws.query_funnel(
["Signup", "Purchase"],
group_by=CohortBreakdown(123, "Power Users"),
)
See Insights Queries — Cohort Breakdowns for inline definitions and options.
Custom Property Breakdowns¶
Break down funnel results by a saved or inline custom property:
from mixpanel_data import GroupBy, InlineCustomProperty
result = ws.query_funnel(
["Signup", "Purchase"],
group_by=GroupBy(
property=InlineCustomProperty.numeric("A * B", A="price", B="quantity"),
property_type="number",
bucket_size=50,
),
)
See Insights Queries — Custom Properties in Queries for CustomPropertyRef and full options.
See Insights Queries — Breakdowns for the full GroupBy reference.
Exclusions¶
Exclude users who perform specific events between funnel steps. Users who trigger an excluded event within the specified step range are removed from the funnel.
String Shorthand¶
Pass event names as strings to exclude them between all steps:
# Exclude users who log out anywhere in the funnel
result = ws.query_funnel(
["Signup", "Add to Cart", "Purchase"],
exclusions=["Logout"],
)
The Exclusion Class¶
For targeted exclusion between specific steps, use Exclusion objects:
from mixpanel_data import Exclusion
result = ws.query_funnel(
["Signup", "Add to Cart", "Checkout", "Purchase"],
exclusions=[
Exclusion("Logout"), # between all steps (same as string)
Exclusion("Refund", from_step=2, to_step=3), # only between Checkout and Purchase
],
)
Exclusion fields:
| Field | Type | Default | Description |
|---|---|---|---|
event |
str |
(required) | Event name to exclude |
from_step |
int |
0 |
Start of exclusion range (0-indexed, inclusive) |
to_step |
int \| None |
None |
End of exclusion range (0-indexed, inclusive). None = last step |
Note
Step indices are 0-based. For a 3-step funnel ["A", "B", "C"], from_step=0, to_step=2 covers the entire funnel.
Holding Constant¶
Hold properties constant across all funnel steps. Only users whose property value is the same at every step are counted as converting.
String Shorthand¶
Pass property names as strings:
# Only count conversions where platform is the same at every step
result = ws.query_funnel(
["Signup", "Purchase"],
holding_constant="platform",
)
# Multiple properties
result = ws.query_funnel(
["Signup", "Purchase"],
holding_constant=["platform", "country"],
)
The HoldingConstant Class¶
For user-profile properties, use HoldingConstant objects:
from mixpanel_data import HoldingConstant
result = ws.query_funnel(
["Signup", "Purchase"],
holding_constant=[
HoldingConstant("platform"), # event property (default)
HoldingConstant("plan_tier", resource_type="people"), # user-profile property
],
)
HoldingConstant fields:
| Field | Type | Default | Description |
|---|---|---|---|
property |
str |
(required) | Property name to hold constant |
resource_type |
"events" \| "people" |
"events" |
Whether this is an event or user-profile property |
Note
Maximum 3 holding constant properties per query.
Time Ranges¶
Relative (Default)¶
By default, query_funnel() returns the last 30 days. Customize with last (always in days) and unit (aggregation granularity):
# Last 7 days
result = ws.query_funnel(["Signup", "Purchase"], last=7)
# Last 84 days (~12 weeks), weekly granularity
result = ws.query_funnel(["Signup", "Purchase"], last=84, unit="week")
# Last 180 days (~6 months), monthly granularity
result = ws.query_funnel(["Signup", "Purchase"], last=180, unit="month")
Absolute¶
Specify explicit start and end dates:
# Q1 2025
result = ws.query_funnel(
["Signup", "Purchase"],
from_date="2025-01-01",
to_date="2025-03-31",
)
Dates must be in YYYY-MM-DD format.
Display Modes¶
The mode parameter controls result presentation:
| Mode | Description | Use case |
|---|---|---|
"steps" (default) |
Step-level conversion data | Standard funnel analysis |
"trends" |
Conversion over time | Track funnel performance trends |
"table" |
Tabular breakdown | Detailed segment comparison |
# Conversion trend over time
result = ws.query_funnel(
["Signup", "Purchase"],
mode="trends",
last=90,
unit="week",
)
Working with Results¶
FunnelQueryResult¶
query_funnel() returns a FunnelQueryResult with:
result = ws.query_funnel(["Signup", "Add to Cart", "Purchase"])
# Overall conversion rate (first to last step)
result.overall_conversion_rate # 0.12 (12%)
# DataFrame (lazy, cached)
result.df
# step event count step_conv_ratio overall_conv_ratio avg_time avg_time_from_start
# 1 1 Signup 1000 1.00 1.00 0.0 0.0
# 2 2 Add to Cart 450 0.45 0.45 1800.0 1800.0
# 3 3 Purchase 120 0.27 0.12 3600.0 5400.0
# Raw step data
result.steps_data # list of dicts with step-level metrics
result.series # raw API series data
# Time range
result.from_date # "2025-03-01"
result.to_date # "2025-03-31"
# Metadata
result.computed_at # "2025-03-31T12:00:00.000000+00:00"
result.meta # {"sampling_factor": 1.0, ...}
# Generated bookmark params (for debugging or persistence)
result.params # dict — the full bookmark JSON sent to API
DataFrame Structure¶
The DataFrame has one row per funnel step:
| Column | Description |
|---|---|
step |
Step number (1-indexed) |
event |
Event name |
count |
Number of users/events reaching this step |
step_conv_ratio |
Conversion rate from previous step (1.0 for first step) |
overall_conv_ratio |
Conversion rate from first step |
avg_time |
Average time from previous step (seconds) |
avg_time_from_start |
Average time from first step (seconds) |
Persisting as a Saved Report¶
The generated bookmark params can be saved as a Mixpanel report:
from mixpanel_data import CreateBookmarkParams
result = ws.query_funnel(["Signup", "Purchase"], conversion_window=7, last=90)
ws.create_bookmark(CreateBookmarkParams(
name="Signup → Purchase Funnel (7d window)",
bookmark_type="funnels",
params=result.params,
))
Debugging¶
Inspect result.params to see the exact bookmark JSON sent to the API:
import json
result = ws.query_funnel(["Signup", "Purchase"])
print(json.dumps(result.params, indent=2))
Validation¶
query_funnel() validates all parameter combinations before making an API call and raises BookmarkValidationError with descriptive messages:
| Rule | Error code | Error message |
|---|---|---|
| Fewer than 2 steps | F1_MIN_STEPS |
At least 2 steps are required |
| More than 100 steps | F1_MAX_STEPS |
Maximum 100 steps allowed |
| Empty step event name | F2_EMPTY_STEP_EVENT |
Step event name must be a non-empty string |
| Control chars in step name | F2_CONTROL_CHAR_STEP_EVENT |
Step event name contains control characters |
| Non-positive conversion window | F3_CONVERSION_WINDOW_POSITIVE |
conversion_window must be a positive integer |
| Window exceeds max for unit | F3_CONVERSION_WINDOW_MAX |
conversion_window exceeds maximum for unit |
| Empty exclusion event | F4_EMPTY_EXCLUSION_EVENT |
Exclusion event name must be non-empty |
| Exclusion step order invalid | F4_EXCLUSION_STEP_ORDER |
to_step must be > from_step |
| Exclusion step out of bounds | F4_EXCLUSION_STEP_BOUNDS |
Step index exceeds step count |
| Invalid conversion window unit | F7_INVALID_WINDOW_UNIT |
Must be one of: second, minute, ... |
| Second unit requires window >= 2 | F7_SECOND_MIN_WINDOW |
Must be at least 2 for seconds |
| More than 3 holding constant | F8_MAX_HOLDING_CONSTANT |
Maximum 3 holding_constant properties |
| Session math without session unit | F9_SESSION_MATH_REQUIRES_SESSION_WINDOW |
Requires conversion_window_unit='session' |
| Property math missing math_property | F10_MATH_MISSING_PROPERTY |
Property-aggregation math types require math_property |
| Non-property math given math_property | F11_MATH_REJECTS_PROPERTY |
Count/rate math types don't accept math_property |
Complete Examples¶
E-Commerce Funnel¶
import mixpanel_data as mp
from mixpanel_data import FunnelStep, Filter, Exclusion, HoldingConstant
ws = mp.Workspace()
# Full purchase funnel with exclusions and filters
result = ws.query_funnel(
[
FunnelStep("View Product"),
FunnelStep("Add to Cart"),
FunnelStep(
"Purchase",
filters=[Filter.greater_than("amount", 0)],
),
],
conversion_window=7,
exclusions=[Exclusion("Remove from Cart", from_step=1, to_step=2)],
holding_constant="platform",
where=Filter.equals("country", "US"),
group_by="platform",
last=90,
)
print(f"Overall conversion: {result.overall_conversion_rate:.1%}")
print(result.df)
Onboarding Funnel¶
# Track user onboarding completion
result = ws.query_funnel(
[
"Create Account",
"Verify Email",
"Complete Profile",
"First Action",
],
conversion_window=3,
conversion_window_unit="day",
order="loose",
math="conversion_rate_unique",
last=30,
unit="week",
mode="trends", # track onboarding trends over time
)
# Identify the biggest drop-off point
for step_data in result.steps_data:
print(f"{step_data['event']}: {step_data['step_conv_ratio']:.0%} step conversion")
Generating Params Without Querying¶
Use build_funnel_params() to generate bookmark params without making an API call — useful for debugging, inspecting the generated JSON, or saving queries as reports:
# Same arguments as query_funnel(), returns dict instead of FunnelQueryResult
params = ws.build_funnel_params(
["Signup", "Add to Cart", "Purchase"],
conversion_window=7,
exclusions=["Logout"],
holding_constant="platform",
last=90,
)
import json
print(json.dumps(params, indent=2)) # inspect the generated bookmark JSON
# Save as a report directly from params
from mixpanel_data import CreateBookmarkParams
ws.create_bookmark(CreateBookmarkParams(
name="Purchase Funnel (7d)",
bookmark_type="funnels",
params=params,
))
Next Steps¶
- Insights Queries — Typed analytics with DAU, formulas, filters, and breakdowns
- Retention Queries — Typed retention analysis with event pairs and custom buckets
- Flow Queries — Typed flow path analysis with steps, directions, and graph output
- Live Analytics — Funnels — Legacy funnel method (saved funnels by ID)
- API Reference — Workspace — Full method signatures
- API Reference — Types — FunnelStep, Exclusion, HoldingConstant, FunnelQueryResult details