diff --git a/dbt/dbt_project.yml b/dbt/dbt_project.yml index a1b391f..8722ed7 100644 --- a/dbt/dbt_project.yml +++ b/dbt/dbt_project.yml @@ -29,5 +29,7 @@ models: marketing_analytics: staging: +materialized: view + intermediate: + +materialized: view marts: +materialized: view \ No newline at end of file diff --git a/dbt/models/intermediate/_intermediate.yml b/dbt/models/intermediate/_intermediate.yml new file mode 100644 index 0000000..fbc541a --- /dev/null +++ b/dbt/models/intermediate/_intermediate.yml @@ -0,0 +1,42 @@ +version: 2 + +models: + - name: int_customer_lifetime_value + description: | + Customer-level aggregation of conversion data. + Grain: One row per user_id. + columns: + - name: user_id + description: Unique identifier for the customer + data_tests: + - not_null + - unique + - name: total_revenue + description: Total conversion value across all purchases + - name: total_orders + description: Number of distinct conversions + - name: first_purchase_date + description: Date of the customer's first purchase + - name: last_purchase_date + description: Date of the customer's most recent purchase + + - name: int_campaign_funnel + description: | + Campaign-level funnel metrics aggregated across all dates. + Grain: One row per campaign_id. + columns: + - name: campaign_id + description: Unique identifier for the campaign + data_tests: + - not_null + - unique + - name: total_impressions + description: Total impressions across all dates + - name: total_clicks + description: Total clicks across all dates + - name: total_sessions + description: Total distinct sessions attributed to this campaign + - name: total_conversions + description: Total distinct conversions attributed to this campaign + - name: total_revenue + description: Total conversion revenue attributed to this campaign diff --git a/dbt/models/intermediate/int_campaign_funnel.sql b/dbt/models/intermediate/int_campaign_funnel.sql new file mode 100644 index 0000000..d37ce32 --- /dev/null +++ b/dbt/models/intermediate/int_campaign_funnel.sql @@ -0,0 +1,50 @@ +with campaigns as ( + select * from {{ ref('stg_campaigns_daily') }} +), + +campaign_totals as ( + select + campaign_id, + sum(impressions) as total_impressions, + sum(clicks) as total_clicks + + from campaigns + group by campaign_id +), + +session_counts as ( + select + campaign_id, + count(distinct session_id) as total_sessions + + from {{ ref('stg_sessions') }} + group by campaign_id +), + +conversion_counts as ( + select + attributed_campaign_id as campaign_id, + count(distinct conversion_id) as total_conversions, + sum(conversion_value) as total_revenue + + from {{ ref('stg_conversions') }} + group by attributed_campaign_id +), + +final as ( + select + ct.campaign_id, + ct.total_impressions, + ct.total_clicks, + coalesce(sc.total_sessions, 0) as total_sessions, + coalesce(cc.total_conversions, 0) as total_conversions, + coalesce(cc.total_revenue, 0) as total_revenue + + from campaign_totals ct + left join session_counts sc + on ct.campaign_id = sc.campaign_id + left join conversion_counts cc + on ct.campaign_id = cc.campaign_id +) + +select * from final diff --git a/dbt/models/intermediate/int_customer_lifetime_value.sql b/dbt/models/intermediate/int_customer_lifetime_value.sql new file mode 100644 index 0000000..673e8d4 --- /dev/null +++ b/dbt/models/intermediate/int_customer_lifetime_value.sql @@ -0,0 +1,17 @@ +with conversions as ( + select * from {{ ref('stg_conversions') }} +), + +customer_metrics as ( + select + user_id, + sum(conversion_value) as total_revenue, + count(distinct conversion_id) as total_orders, + min(date) as first_purchase_date, + max(date) as last_purchase_date + + from conversions + group by user_id +) + +select * from customer_metrics diff --git a/dbt/models/marts/_marts.yml b/dbt/models/marts/_marts.yml index 6b8ad95..afabd24 100644 --- a/dbt/models/marts/_marts.yml +++ b/dbt/models/marts/_marts.yml @@ -154,3 +154,63 @@ models: - name: overall_cpa description: Overall cost per acquisition (total spend / total conversions) + - name: customer_lifetime_value + description: | + Customer lifetime value with acquisition channel and tier segmentation. + Answers: "How much is each customer worth and where did they come from?" + + Grain: One row per user_id + columns: + - name: user_id + description: Unique identifier for the customer + data_tests: + - not_null + - unique + - name: total_revenue + description: Total conversion value across all purchases + - name: total_orders + description: Number of distinct purchases + - name: first_purchase_date + description: Date of the customer's first purchase + - name: last_purchase_date + description: Date of the customer's most recent purchase + - name: acquisition_channel + description: Channel of the customer's earliest first-touch attribution touchpoint + - name: ltv_tier + description: "Customer segment: high ($500+), medium ($100-499), low (under $100)" + + - name: campaign_funnel_analysis + description: | + Campaign funnel with stage-to-stage conversion rates and efficiency score. + Answers: "Where are we losing people in the funnel, per campaign?" + + Grain: One row per campaign_id + columns: + - name: campaign_id + description: Unique identifier for the campaign + data_tests: + - not_null + - unique + - name: campaign_name + description: Name of the campaign + - name: channel + description: Marketing channel + - name: total_impressions + description: Total impressions across all dates + - name: total_clicks + description: Total clicks across all dates + - name: total_sessions + description: Total sessions attributed to this campaign + - name: total_conversions + description: Total conversions attributed to this campaign + - name: total_revenue + description: Total revenue from conversions + - name: impression_to_click_rate + description: Click-through rate (clicks / impressions) + - name: click_to_session_rate + description: Rate of clicks that became sessions + - name: session_to_conversion_rate + description: Rate of sessions that converted + - name: efficiency_score + description: Overall funnel throughput (conversions / impressions) for ranking campaigns + diff --git a/dbt/models/marts/campaign_funnel_analysis.sql b/dbt/models/marts/campaign_funnel_analysis.sql new file mode 100644 index 0000000..b018d9e --- /dev/null +++ b/dbt/models/marts/campaign_funnel_analysis.sql @@ -0,0 +1,54 @@ +with funnel as ( + select * from {{ ref('int_campaign_funnel') }} +), + +campaign_details as ( + select distinct + campaign_id, + campaign_name, + channel + + from {{ ref('stg_campaigns_daily') }} +), + +final as ( + select + f.campaign_id, + cd.campaign_name, + cd.channel, + + -- Funnel counts + f.total_impressions, + f.total_clicks, + f.total_sessions, + f.total_conversions, + f.total_revenue, + + -- Stage-to-stage conversion rates + case + when f.total_impressions > 0 then (f.total_clicks::float / f.total_impressions::float) + else 0 + end as impression_to_click_rate, + + case + when f.total_clicks > 0 then (f.total_sessions::float / f.total_clicks::float) + else 0 + end as click_to_session_rate, + + case + when f.total_sessions > 0 then (f.total_conversions::float / f.total_sessions::float) + else 0 + end as session_to_conversion_rate, + + -- Overall efficiency score + case + when f.total_impressions > 0 then (f.total_conversions::float / f.total_impressions::float) + else 0 + end as efficiency_score + + from funnel f + left join campaign_details cd + on f.campaign_id = cd.campaign_id +) + +select * from final diff --git a/dbt/models/marts/customer_lifetime_value.sql b/dbt/models/marts/customer_lifetime_value.sql new file mode 100644 index 0000000..81e2877 --- /dev/null +++ b/dbt/models/marts/customer_lifetime_value.sql @@ -0,0 +1,44 @@ +with customer_metrics as ( + select * from {{ ref('int_customer_lifetime_value') }} +), + +first_touch_ranked as ( + select + user_id, + channel as acquisition_channel, + row_number() over (partition by user_id order by timestamp asc) as rn + + from {{ ref('stg_attribution_touchpoints') }} + where touchpoint_position = 1 +), + +first_touch as ( + select + user_id, + acquisition_channel + + from first_touch_ranked + where rn = 1 +), + +final as ( + select + cm.user_id, + cm.total_revenue, + cm.total_orders, + cm.first_purchase_date, + cm.last_purchase_date, + coalesce(ft.acquisition_channel, 'unknown') as acquisition_channel, + + case + when cm.total_revenue >= 500 then 'high' + when cm.total_revenue >= 100 then 'medium' + else 'low' + end as ltv_tier + + from customer_metrics cm + left join first_touch ft + on cm.user_id = ft.user_id +) + +select * from final