Tutorial

Product Analytics in Python: Funnel Conversion, Drop-off & User Journey Mapping

Master product funnel analysis in Python with Pandas and Plotly. Calculate step-by-step conversion rates, drop-off percentages, time-to-convert distributions, and interactive Sankey user journeys.

Anuj SainiAug 24, 202611 min read

In digital products, user conversion is rarely a straight line. Every click, form field, and screen transition introduces friction where prospective customers can drop off — a challenge commonly known across growth and product engineering teams as the "Leaky Bucket" problem.

While SQL is great for extracting raw clickstream event tables (see our SQL Window Functions Guide), Python gives product analysts and analytics engineers the full programmatic power needed to calculate granular conversion velocity, eliminate event sequencing anomalies, segment cohorts by device or channel, and render interactive visuals.



Interactive Practice

Want to sharpen your Python product analytics skills? Practice data aggregation, grouping, and cohort metrics directly in your browser on Topfolio Practice with instant automated evaluation.

1. Anatomy of Clickstream Event Logs

In modern data platforms (Snowflake, BigQuery, ClickHouse, or PostHog/Segment pipelines), event data arrives as an append-only clickstream log. Each row represents a discrete action taken by a user at a given timestamp.

Let's simulate a realistic clickstream dataset representing 10,000 visitors navigating an e-commerce platform across five core stages:

python
import pandas as pd
import numpy as np
import plotly.express as px
import plotly.graph_objects as go
from datetime import datetime, timedelta
 
# Set seed for reproducible data generation
np.random.seed(42)
n_users = 10000
 
# Funnel step definitions in chronological order
funnel_steps = [
    '1_Landing_Page',
    '2_Signup',
    '3_Product_Search',
    '4_Add_to_Cart',
    '5_Purchase'
]
 
# Step-to-next-step transition probabilities
transition_probs = {
    '1_Landing_Page': 0.72,   # 72% proceed to Signup
    '2_Signup': 0.58,         # 58% search for a product
    '3_Product_Search': 0.44, # 44% add an item to cart
    '4_Add_to_Cart': 0.65     # 65% complete checkout
}
 
events = []
base_time = datetime(2026, 8, 1, 9, 0, 0)
 
for user_id in range(1001, 1001 + n_users):
    device = np.random.choice(['Mobile', 'Desktop', 'Tablet'], p=[0.60, 0.32, 0.08])
    current_time = base_time + timedelta(seconds=int(np.random.exponential(scale=3600 * 24)))
    
    # Step 1: Every user hits the landing page
    events.append({
        'user_id': user_id,
        'step_name': '1_Landing_Page',
        'timestamp': current_time,
        'device': device
    })
    
    # Simulate sequential funnel progression
    for current_step, next_step in zip(funnel_steps[:-1], funnel_steps[1:]):
        if np.random.random() < transition_probs[current_step]:
            # Add realistic delay between actions (15s to 8 mins)
            step_delay_sec = int(np.random.gamma(shape=2.5, scale=60))
            current_time += timedelta(seconds=step_delay_sec)
            
            events.append({
                'user_id': user_id,
                'step_name': next_step,
                'timestamp': current_time,
                'device': device
            })
        else:
            # User dropped off at this stage
            break
 
df_events = pd.DataFrame(events)
print(f"Total Logged Events: {len(df_events):,}")
print(f"Unique Users: {df_events['user_id'].nunique():,}")
df_events.head(8)

Event Log Sample Output

user_idstep_nametimestampdevice
10011_Landing_Page2026-08-01 10:14:22Mobile
10012_Signup2026-08-01 10:17:05Mobile
10013_Product_Search2026-08-01 10:19:48Mobile
10014_Add_to_Cart2026-08-01 10:22:10Mobile
10015_Purchase2026-08-01 10:24:55Mobile
10021_Landing_Page2026-08-01 11:02:11Desktop
10022_Signup2026-08-01 11:04:30Desktop
10031_Landing_Page2026-08-01 11:15:00Mobile

2. Calculating Conversion & Drop-off in Pandas

To build an accurate funnel table, we must aggregate distinct users (nunique()) at each stage, sort them by stage order, and compute step-over-step and cumulative metrics.

Mathematical Definitions

  1. Step-over-Step Conversion Rate (CR_step): CR_step(i) = Users(i) / Users(i-1)

  2. Drop-off Rate: DropOff(i) = 1 - CR_step(i) = (Users(i-1) - Users(i)) / Users(i-1)

  3. Cumulative Conversion Rate (CR_cum): CR_cum(i) = Users(i) / Users(0)

Implementation in Python

python
# 1. Count unique users per step
funnel_df = (
    df_events.groupby('step_name')['user_id']
    .nunique()
    .reset_index()
    .rename(columns={'step_name': 'Step', 'user_id': 'Unique_Users'})
)
 
# 2. Sort by the numeric prefix in Step name
funnel_df = funnel_df.sort_values('Step').reset_index(drop=True)
 
# 3. Calculate step-over-step conversion and drop-off
top_of_funnel_users = funnel_df.loc[0, 'Unique_Users']
 
funnel_df['Prev_Step_Users'] = funnel_df['Unique_Users'].shift(1)
funnel_df['Step_Conversion_Pct'] = (
    (funnel_df['Unique_Users'] / funnel_df['Prev_Step_Users']) * 100
).fillna(100.0)
 
funnel_df['Step_Dropoff_Pct'] = (
    (1 - (funnel_df['Unique_Users'] / funnel_df['Prev_Step_Users'])) * 100
).fillna(0.0)
 
funnel_df['Cumulative_Conversion_Pct'] = (
    (funnel_df['Unique_Users'] / top_of_funnel_users) * 100
)
 
# 4. Clean formatting for reporting
formatted_funnel = funnel_df.copy()
formatted_funnel['Step_Conversion_Pct'] = formatted_funnel['Step_Conversion_Pct'].map('{:.2f}%'.format)
formatted_funnel['Step_Dropoff_Pct'] = formatted_funnel['Step_Dropoff_Pct'].map('{:.2f}%'.format)
formatted_funnel['Cumulative_Conversion_Pct'] = formatted_funnel['Cumulative_Conversion_Pct'].map('{:.2f}%'.format)
 
formatted_funnel[['Step', 'Unique_Users', 'Step_Conversion_Pct', 'Step_Dropoff_Pct', 'Cumulative_Conversion_Pct']]

Conversion Metrics Table

StepUnique_UsersStep_Conversion_PctStep_Dropoff_PctCumulative_Conversion_Pct
1_Landing_Page10,000100.00%0.00%100.00%
2_Signup7,20472.04%27.96%72.04%
3_Product_Search4,16857.86%42.14%41.68%
4_Add_to_Cart1,84244.19%55.81%18.42%
5_Purchase1,19564.88%35.12%11.95%

Key Insight

Notice the biggest bottleneck: 3_Product_Search4_Add_to_Cart suffers a 55.81% drop-off. Less than half of the users who search for a product end up adding an item to their cart. This immediately signals a product discovery problem (e.g., irrelevant search results, missing filters, or high price friction).


3. Visualizing Funnels with Plotly

Plotly provides two main interfaces for funnel charts: plotly.express.funnel (for quick visual inspection) and plotly.graph_objects.Funnel (for granular dashboard customization).

A. Quick Visual with Plotly Express

python
fig_px = px.funnel(
    funnel_df,
    x='Unique_Users',
    y='Step',
    title='E-Commerce Product Conversion Funnel',
    labels={'Unique_Users': 'Active Users', 'Step': 'Funnel Stage'},
    color_discrete_sequence=['#0d9488']
)
fig_px.update_layout(template='plotly_white')
fig_px.show()

B. Production-Ready Segmented Funnel with go.Funnel

In real product analysis, conversion rates differ drastically by device type (e.g., Mobile checkout vs Desktop checkout). We can segment the funnel using go.Figure with multiple go.Funnel traces:

python
# Aggregate unique users per step by device
device_funnel = (
    df_events.groupby(['step_name', 'device'])['user_id']
    .nunique()
    .reset_index()
    .sort_values(['step_name'])
)
 
fig_segmented = go.Figure()
 
colors = {'Mobile': '#0d9488', 'Desktop': '#6366f1', 'Tablet': '#f59e0b'}
 
for device in ['Mobile', 'Desktop', 'Tablet']:
    sub_df = device_funnel[device_funnel['device'] == device]
    
    fig_segmented.add_trace(go.Funnel(
        name=device,
        y=sub_df['step_name'],
        x=sub_df['user_id'],
        textinfo="value+percent initial+percent previous",
        marker={"color": colors[device]}
    ))
 
fig_segmented.update_layout(
    title='Segmented Funnel Conversion by Device Type',
    template='plotly_white',
    legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1)
)
 
fig_segmented.show()

4. Time-to-Convert & Velocity Analysis

Conversion percentage only tells half the story. A checkout funnel might have an 80% conversion rate, but if users take 45 minutes to fill out forms, conversion will collapse over time.

Calculating Time-to-Convert (TTC) allows teams to measure friction and optimize conversion velocity.

python
# 1. Extract first timestamp for each step per user
user_step_times = (
    df_events.groupby(['user_id', 'step_name'])['timestamp']
    .min()
    .unstack(level='step_name')
)
 
# 2. Compute time deltas in minutes between consecutive steps
ttc_df = pd.DataFrame(index=user_step_times.index)
ttc_df['Landing_to_Signup_Min'] = (
    user_step_times['2_Signup'] - user_step_times['1_Landing_Page']
).dt.total_seconds() / 60.0
 
ttc_df['Signup_to_Search_Min'] = (
    user_step_times['3_Product_Search'] - user_step_times['2_Signup']
).dt.total_seconds() / 60.0
 
ttc_df['Search_to_Cart_Min'] = (
    user_step_times['4_Add_to_Cart'] - user_step_times['3_Product_Search']
).dt.total_seconds() / 60.0
 
ttc_df['Cart_to_Purchase_Min'] = (
    user_step_times['5_Purchase'] - user_step_times['4_Add_to_Cart']
).dt.total_seconds() / 60.0
 
ttc_df['Total_Journey_Min'] = (
    user_step_times['5_Purchase'] - user_step_times['1_Landing_Page']
).dt.total_seconds() / 60.0
 
# 3. Calculate summary statistics (Median, 75th percentile, 90th percentile)
velocity_summary = pd.DataFrame({
    'Metric': [
        'Landing -> Signup',
        'Signup -> Search',
        'Search -> Add to Cart',
        'Add to Cart -> Purchase',
        'Full Journey (Landing -> Purchase)'
    ],
    'Median_Time_Min': [
        ttc_df['Landing_to_Signup_Min'].median(),
        ttc_df['Signup_to_Search_Min'].median(),
        ttc_df['Search_to_Cart_Min'].median(),
        ttc_df['Cart_to_Purchase_Min'].median(),
        ttc_df['Total_Journey_Min'].median()
    ],
    'P75_Time_Min': [
        ttc_df['Landing_to_Signup_Min'].quantile(0.75),
        ttc_df['Signup_to_Search_Min'].quantile(0.75),
        ttc_df['Search_to_Cart_Min'].quantile(0.75),
        ttc_df['Cart_to_Purchase_Min'].quantile(0.75),
        ttc_df['Total_Journey_Min'].quantile(0.75)
    ],
    'P90_Time_Min': [
        ttc_df['Landing_to_Signup_Min'].quantile(0.90),
        ttc_df['Signup_to_Search_Min'].quantile(0.90),
        ttc_df['Search_to_Cart_Min'].quantile(0.90),
        ttc_df['Cart_to_Purchase_Min'].quantile(0.90),
        ttc_df['Total_Journey_Min'].quantile(0.90)
    ]
})
 
velocity_summary.round(2)

Time-to-Convert Benchmark Table

MetricMedian_Time_MinP75_Time_MinP90_Time_Min
Landing → Signup2.153.204.60
Signup → Search2.203.184.55
Search → Add to Cart2.183.254.70
Add to Cart → Purchase2.223.304.68
Full Journey (Landing → Purchase)8.7512.4016.85

5. Visualizing Multi-Step Paths with Sankey Diagrams

Linear funnels assume users progress in strict sequential order. In reality, users drop off, re-search, navigate back to previous pages, or leave entirely. A Sankey diagram explicitly models node transitions and drop-off sinks.

python
# Prepare Sankey flow indices (Source -> Target -> Volume)
label_list = funnel_df['Step'].tolist() + ['Drop_Off_Exit']
label_map = {name: i for i, name in enumerate(label_list)}
 
drop_off_node_idx = label_map['Drop_Off_Exit']
 
sources = []
targets = []
values = []
link_colors = []
 
for i in range(len(funnel_df) - 1):
    current_step = funnel_df.loc[i, 'Step']
    next_step = funnel_df.loc[i + 1, 'Step']
    
    current_users = funnel_df.loc[i, 'Unique_Users']
    converted_users = funnel_df.loc[i + 1, 'Unique_Users']
    dropped_users = current_users - converted_users
    
    # 1. Flow of users who successfully converted to next step
    sources.append(label_map[current_step])
    targets.append(label_map[next_step])
    values.append(converted_users)
    link_colors.append('rgba(13, 148, 136, 0.45)') # Teal
    
    # 2. Flow of users who dropped off at this stage
    sources.append(label_map[current_step])
    targets.append(drop_off_node_idx)
    values.append(dropped_users)
    link_colors.append('rgba(239, 68, 68, 0.35)') # Red
    
fig_sankey = go.Figure(data=[go.Sankey(
    node=dict(
        pad=18,
        thickness=24,
        line=dict(color="black", width=0.5),
        label=label_list,
        color=['#0f766e', '#0d9488', '#14b8a6', '#2dd4bf', '#5eead4', '#ef4444']
    ),
    link=dict(
        source=sources,
        target=targets,
        value=values,
        color=link_colors
    )
)])
 
fig_sankey.update_layout(
    title_text="User Journey Conversion Flow & Drop-off Sink (Sankey)",
    font_size=12,
    template='plotly_white'
)
fig_sankey.show()

6. Common Funnel Pitfalls & Best Practices

When analyzing product funnels in production, keep these four technical rules in mind:

1. Enforce Event Ordering with Timestamps

Do not rely solely on step_name string matching. Ensure that timestamp_step_B > timestamp_step_A. If a user purchased first and visited the search page later during the day, treating the search event as the top of the funnel introduces severe survivor bias.

2. Isolate User-Level vs Session-Level Funnels

  • Session Funnel: All events must occur within the same session_id (typically reset after 30 minutes of inactivity). Useful for quick impulse purchases.
  • User Funnel: Events can span multiple sessions within a 7-day or 30-day attribution window. Crucial for high-consideration B2B SaaS purchases or expensive retail items.

3. Deduplicate Rapid-Fire Client Events

Users frequently double-click buttons or refresh loading screens. Always apply .drop_duplicates(subset=['user_id', 'step_name', 'session_id']) before computing conversion rates.

4. Connect Funnel Steps to A/B Testing

Once you pinpoint a drop-off step (like our 55.8% drop on Search), run an A/B test specifically targeting that transition (e.g., adding personalized autocomplete search recommendations) and measure if the step conversion delta is statistically significant.


Summary & Next Steps

You now have a complete, production-ready framework for product funnel analysis in Python:

  1. Clean clickstream event tables with unique user counting.
  2. Calculate Step-over-Step conversion and drop-off using .pct_change() and .shift().
  3. Visualize conversion flows with interactive Plotly go.Funnel and go.Sankey charts.
  4. Measure friction and completion speed with time-to-convert percentile distributions.

To further elevate your data analytics capabilities, explore our hands-on guides:

Frequently Asked Questions

What is the difference between Step-over-Step Conversion and Cumulative Conversion?

Step-over-step conversion measures the percentage of users who advance from the immediate previous stage (Users[N] / Users[N-1]), identifying pinpoint friction between adjacent screens. Cumulative conversion measures the percentage of users retained from the very top of the funnel (Users[N] / Users[0]), showing overall baseline health.

How do you handle users who skip non-mandatory funnel steps?

Product funnels are categorized as closed (strict sequential order where users must complete step N before step N+1) or open (users can enter or complete steps in arbitrary sequence). In Python, closed funnels filter timestamps sequentially (timestamp[N] > timestamp[N-1]), whereas open funnels simply measure aggregate event participation within a defined session window.

Why must funnel analysis count distinct users rather than raw event counts?

Raw event counts artificially inflate conversion metrics if highly active users trigger an action multiple times (e.g., repeatedly clicking 'Search' or refreshing 'Product Page'). Counting unique user IDs (df.groupby('step')['user_id'].nunique()) ensures every individual represents exactly one conversion or drop-off.

What is an attribution window in funnel analytics?

An attribution window (or conversion window) defines the maximum allowable elapsed time between the first step and subsequent steps (e.g., 1 hour, 24 hours, or 7 days). Events occurring after this timeout are excluded from the conversion calculation to prevent attributing late purchases to an unrelated earlier session.

When should you use a Sankey diagram instead of a standard funnel chart?

A standard funnel chart assumes a single linear progression through fixed stages. A Sankey diagram visualizes multi-directional flows, loops, alternative navigation branches (e.g., users abandoning checkout to apply promo codes or search again), and unexpected drop-off exit paths.

Anuj Saini

Written by

Anuj SainiFounder & Lead Instructor

Founder at Topfolio with 6+ years in data & analytics across JPMC, Ultrahuman, and high-growth startups. Sat on hiring panels, reviewed 500+ resumes, and writes practical SQL & data guides.