Clickstream Funnel Analysis for Growth Teams

Estimated read: 4 min

Growth doesn’t care about completed orders alone – it cares about everyone who didn’t finish checking out. bronze_clickstream_valid is the only input Section 1’s plan gave three event types for: browse, add_to_cart, and purchase, one row per event, generated several times over per order the way a real storefront actually behaves. This lecture turns that raw event stream into a funnel.

Why this reads bronze, not silver, again

Clickstream is the second table in this section – after inventory – that skips silver entirely, for the same reason Section 3’s design note gave: no StepRight-specific business rule beyond bronze’s structural quarantine applies to a page-view event. bronze_clickstream_valid is already the clean input this materialized view needs.

Counting each stage of the funnel

# transformations/gold_reporting.py (addition)
from pyspark.sql.functions import col, countDistinct, when

@dp.materialized_view(
    name="gold_clickstream_funnel",
    comment="Daily browse -> add-to-cart -> purchase funnel by product category, for growth"
)
def gold_clickstream_funnel():
    clickstream = spark.read.table("bronze_clickstream_valid")
    products = spark.read.table("silver_products")

    events = clickstream.join(products, "product_id", "left")

    return (
        events
        .groupBy(col("event_timestamp").cast("date").alias("event_date"), "category")
        .agg(
            countDistinct(when(col("event_type") == "browse", col("session_id"))).alias("browse_sessions"),
            countDistinct(when(col("event_type") == "add_to_cart", col("session_id"))).alias("cart_sessions"),
            countDistinct(when(col("event_type") == "purchase", col("session_id"))).alias("purchase_sessions"),
        )
    )

countDistinct on session_id, gated by a when per stage, is what turns three event types into three funnel counts in one pass over the same joined DataFrame – no need to filter and re-aggregate the same table three separate times. Counting sessions, not raw events, matters here: a single browsing session that views the same product five times should count once toward browse_sessions, not five times, or the funnel overstates top-of-funnel traffic relative to the stages below it.

Conversion rates as a derived view, not a stored column

gold_clickstream_funnel deliberately stops at raw counts rather than storing a computed conversion_rate column – a ratio derived from two other columns already in the table is cheap to compute at query time and never risks drifting out of sync with the counts it’s based on:

SELECT
    event_date,
    category,
    browse_sessions,
    cart_sessions,
    purchase_sessions,
    ROUND(cart_sessions / NULLIF(browse_sessions, 0), 3) AS browse_to_cart_rate,
    ROUND(purchase_sessions / NULLIF(cart_sessions, 0), 3) AS cart_to_purchase_rate
FROM dev.step_right.gold_clickstream_funnel
ORDER BY event_date DESC;

NULLIF(browse_sessions, 0) guards the same division-by-zero case Lecture 3’s days_of_supply handled with a when – a category with zero browse sessions on a given day should produce a null rate, not an error or a misleading zero.

Where the funnel narrows, and why the left join matters

events joins clickstream to products as left, not inner – a purchase event’s product_id should always resolve, since it came from an order that also passed silver’s referential checks, but an early browse event can reference a product a shopper viewed and StepRight later delisted. Losing that row to an inner join would understate browse_sessions for a category that’s otherwise performing well; keeping it with a null category groups it separately instead, visible rather than silently dropped.

Verifying the result

-- Categories with a sharp cart-to-purchase drop -- growth's highest-priority investigation list
SELECT category, SUM(cart_sessions) AS carts, SUM(purchase_sessions) AS purchases
FROM dev.step_right.gold_clickstream_funnel
GROUP BY category
HAVING SUM(cart_sessions) > 0
ORDER BY (SUM(purchase_sessions) / SUM(cart_sessions)) ASC;

What’s next

The funnel tracks a shopper up to the purchase. Lecture 5 picks up exactly where this one leaves off – what happens to a product after the purchase event fires.


| ← Previous: Product Performance and Sales Velocity for Merchandising | Next: Product Delivery and Fulfilment Health for Operations → | |:—|—:|


Back to top

Independent, self-authored Databricks companion notes.

This site uses Just the Docs, a documentation theme for Jekyll.