From ca5be5c3cf5ff8123765ae115433b5af27d976c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oriol=20Roqu=C3=A9=20Paniagua?= Date: Mon, 28 Apr 2025 09:36:25 +0000 Subject: [PATCH] Merged PR 5069: Account revenue impact from growth in a monthly basis # Description Creates an intermediate model to compute the impact in revenue (total or rrpr) due to the growth. This model also categorises the impact, and provides additional attributes for reporting purposes. This model aims to substitute, in time, the current existing version of `int_monthly_growth_score_by_deal`. # Checklist - [X] The edited models and dependants run properly with production data. - [X] The edited models are sufficiently documented. - [X] The edited models contain PK tests, and I've ran and passed them. - [X] I have checked for DRY opportunities with other models and docs. - [X] I've picked the right materialization for the affected models. # Other - [ ] Check if a full-refresh is required after this PR is merged. Related work items: #29374 --- ...hly_account_revenue_impact_from_growth.sql | 250 +++++++++++++++ models/intermediate/cross/schema.yml | 293 ++++++++++++++++++ 2 files changed, 543 insertions(+) create mode 100644 models/intermediate/cross/int_monthly_account_revenue_impact_from_growth.sql diff --git a/models/intermediate/cross/int_monthly_account_revenue_impact_from_growth.sql b/models/intermediate/cross/int_monthly_account_revenue_impact_from_growth.sql new file mode 100644 index 0000000..79258bf --- /dev/null +++ b/models/intermediate/cross/int_monthly_account_revenue_impact_from_growth.sql @@ -0,0 +1,250 @@ +{{ config(materialized="table", unique_key=["end_date", "id_deal"]) }} +with + int_billable_items_growth_score_by_deal as ( + select * from {{ ref("int_billable_items_growth_score_by_deal") }} + ), + int_kpis__agg_monthly_total_and_retained_revenue as ( + select * from {{ ref("int_kpis__agg_monthly_total_and_retained_revenue") }} + ), + int_kpis__agg_dates_main_kpis as ( + select * from {{ ref("int_kpis__agg_dates_main_kpis") }} + ), + int_kpis__dimension_deals as (select * from {{ ref("int_kpis__dimension_deals") }}), + int_hubspot__deal as (select * from {{ ref("int_hubspot__deal") }}), + int_kpis__lifecycle_daily_deal as ( + select * from {{ ref("int_kpis__lifecycle_daily_deal") }} + ), + billable_items_growth_score_refined as ( + select + big.start_date, + big.end_date, + big.id_deal, + big.current_month_billable_items, + big.current_month_share_billable_items, + case + when d.cancellation_date_utc between big.start_date and big.end_date + then -1 + else big.growth_score + end as growth_score, + big.projection_mean_absolute_error, + big.projection_mean_absolute_percentage_error, + big.are_billable_items_projected, + case + when d.cancellation_date_utc between big.start_date and big.end_date + then true + else false + end as is_growth_score_overridden_due_to_cancellation + from int_billable_items_growth_score_by_deal big + left join int_hubspot__deal d on big.id_deal = d.id_deal + ), + revenue_per_deal_and_month as ( + select + d.date, + d.dimension_value as id_deal, + -- Metrics current month + coalesce(r.total_revenue_in_gbp, 0) as current_month_total_revenue_in_gbp, + coalesce( + r.revenue_retained_post_resolutions_in_gbp, 0 + ) as current_month_revenue_retained_post_resolutions_in_gbp, + -- Metrics rolling 12 months - from 11 months ago to current month, + -- inclusive + sum(coalesce(r.total_revenue_in_gbp, 0)) over ( + partition by d.dimension_value + order by d.date asc + rows between 11 preceding and current row + ) as rolling_12_months_total_revenue_in_gbp, + sum(coalesce(r.revenue_retained_post_resolutions_in_gbp, 0)) over ( + partition by d.dimension_value + order by d.date asc + rows between 11 preceding and current row + ) as rolling_12_months_revenue_retained_post_resolutions_in_gbp + from int_kpis__agg_dates_main_kpis d + left join + int_kpis__agg_monthly_total_and_retained_revenue r + on d.dimension_value = r.dimension_value + and d.date = r.end_date + where + d.dimension = 'by_deal' + and d.dimension_value <> 'UNSET' + and d.is_end_of_month = true + ), + deal_revenue_contribution_per_month as ( + select + rpd.date, + -- Transform to next end of month for future join: revenue contribution + -- needs to be multiplied by the current month growth + ( + date_trunc('month', rpd.date)::date + + interval '2 months' + - interval '1 day' + )::date as next_end_of_month, + rpd.id_deal, + -- Deal contribution to metric vs. global + ( + rpd.rolling_12_months_total_revenue_in_gbp + ) / sum(rpd.rolling_12_months_total_revenue_in_gbp) over ( + partition by rpd.date order by rpd.date + ) as share_total_revenue_rolling_12_months, + (rpd.rolling_12_months_revenue_retained_post_resolutions_in_gbp) + / sum(rpd.rolling_12_months_revenue_retained_post_resolutions_in_gbp) over ( + partition by rpd.date order by rpd.date + ) as share_revenue_retained_post_resolutions_rolling_12_months + from revenue_per_deal_and_month rpd + ), + growth_and_impact_scores_per_deal_and_month as ( + select + -- Time window + date_trunc('month', drc.next_end_of_month)::date as start_date, + drc.next_end_of_month as end_date, + + -- Deal + drc.id_deal, + + -- Deal contribution to revenue + drc.share_total_revenue_rolling_12_months, + drc.share_revenue_retained_post_resolutions_rolling_12_months, + + -- Growth Score: if Growth is not available, set to 0 + coalesce(big.growth_score, 0) as growth_score, + + -- Impact Score + coalesce(big.growth_score, 0) + * share_total_revenue_rolling_12_months as impact_score_total_revenue, + coalesce(big.growth_score, 0) + * share_revenue_retained_post_resolutions_rolling_12_months + as impact_score_revenue_retained_post_resolutions, + + -- If billable items are not available, set to 0 + coalesce( + big.current_month_billable_items, 0 + ) as current_month_billable_items, + coalesce( + big.current_month_share_billable_items, 0 + ) as share_billable_items_current_month, + + -- Projection metrics + sum(coalesce(big.current_month_billable_items, 0)) over ( + partition by drc.date order by drc.date + ) as global_monthly_billable_items, + big.projection_mean_absolute_error, + big.projection_mean_absolute_percentage_error, + big.are_billable_items_projected, + big.is_growth_score_overridden_due_to_cancellation + + from deal_revenue_contribution_per_month drc + left join + billable_items_growth_score_refined big + on drc.next_end_of_month = big.end_date + and drc.id_deal = big.id_deal + ) +select + -- Time window + gai.start_date, + gai.end_date, + + -- Deal Static Attributes + gai.id_deal, + gai.id_deal || '-' || coalesce(ikdd.main_deal_name, '') as deal, + ikdd.client_type, + ikdd.has_active_pms, + ikdd.active_pms_list, + ikdd.main_billing_country_iso_3_per_deal, + + -- Deal Lifecycle -- + deal_lifecycle.deal_lifecycle_state, + + -- Deal Hubspot Attributes -- + d.deal_hubspot_stage, + d.account_manager, + d.live_date_utc, + d.cancellation_date_utc, + + -- Growth & Impact Scores + gai.growth_score, + gai.impact_score_total_revenue, + gai.impact_score_revenue_retained_post_resolutions, + + -- Categorisation + case + when impact_score_revenue_retained_post_resolutions <= -0.3 / 100 + then 'MAJOR DECLINE' + when + impact_score_revenue_retained_post_resolutions < -0.03 / 100 + and impact_score_revenue_retained_post_resolutions > -0.3 / 100 + then 'DECLINE' + when + impact_score_revenue_retained_post_resolutions >= -0.03 / 100 + and impact_score_revenue_retained_post_resolutions <= 0.03 / 100 + then 'FLAT' + when + impact_score_revenue_retained_post_resolutions > 0.03 / 100 + and impact_score_revenue_retained_post_resolutions < 0.3 / 100 + then 'GAIN' + when impact_score_revenue_retained_post_resolutions >= 0.3 / 100 + then 'MAJOR GAIN' + else 'UNSET' + end as categorisation_impact_score_revenue_retained_post_resolutions, + + -- Rank per Impact Score + row_number() over ( + partition by gai.end_date + order by gai.impact_score_total_revenue desc, gai.id_deal + ) as rank_impact_score_total_revenue, + row_number() over ( + partition by gai.end_date + order by gai.impact_score_revenue_retained_post_resolutions desc, gai.id_deal + ) as rank_impact_score_revenue_retained_post_resolutions, + + -- Total Revenue Metrics + rpd.current_month_total_revenue_in_gbp, + rpd.rolling_12_months_total_revenue_in_gbp, + gai.share_total_revenue_rolling_12_months, + row_number() over ( + partition by gai.end_date + order by gai.share_total_revenue_rolling_12_months desc, gai.id_deal + ) as rank_total_revenue_rolling_12_months, + + -- Revenue Retained Post Resolutions Metrics + rpd.current_month_revenue_retained_post_resolutions_in_gbp, + rpd.rolling_12_months_revenue_retained_post_resolutions_in_gbp, + gai.share_revenue_retained_post_resolutions_rolling_12_months, + row_number() over ( + partition by gai.end_date + order by + gai.share_revenue_retained_post_resolutions_rolling_12_months desc, + gai.id_deal + ) as rank_revenue_retained_post_resolutions_rolling_12_months, + + -- Billable Items Metrics + gai.current_month_billable_items, + gai.share_billable_items_current_month, + row_number() over ( + partition by gai.end_date + order by gai.current_month_billable_items desc, gai.id_deal + ) as rank_billable_items_current_month, + gai.projection_mean_absolute_error, + gai.projection_mean_absolute_percentage_error, + gai.are_billable_items_projected, + gai.is_growth_score_overridden_due_to_cancellation + +from growth_and_impact_scores_per_deal_and_month gai +left join + revenue_per_deal_and_month rpd + -- Keep Revenue attributed to the real month (will be null in the ongoing month) + on gai.id_deal = rpd.id_deal + and gai.end_date = rpd.date +left join int_kpis__dimension_deals ikdd on gai.id_deal = ikdd.id_deal +left join int_hubspot__deal d on gai.id_deal = d.id_deal +left join + int_kpis__lifecycle_daily_deal deal_lifecycle + -- Retrieve Deal Lifecycle State for the end of the month or yesterday if it's the + -- ongoing month + on ( + gai.end_date = deal_lifecycle.date + or ( + current_date - interval '1 day' = deal_lifecycle.date + and date_trunc('month', gai.end_date)::date + = date_trunc('month', deal_lifecycle.date)::date + ) + ) + and gai.id_deal = deal_lifecycle.id_deal diff --git a/models/intermediate/cross/schema.yml b/models/intermediate/cross/schema.yml index 0566335..66b8089 100644 --- a/models/intermediate/cross/schema.yml +++ b/models/intermediate/cross/schema.yml @@ -3504,3 +3504,296 @@ models: - dbt_expectations.expect_column_values_to_be_between: min_value: 1 strictly: false + + - name: int_monthly_account_revenue_impact_from_growth + description: | + This model computes the monthly revenue impact from the growth of + billable items for each deal. The revenue impact is computed as the + product of the growth score and the deal contribution to the total revenue + in the previous 12 months. + + There's 2 impact scores computed depending on the revenue metric, namely: + - impact_score_total_revenue: based on Total Revenue + - impact_score_revenue_retained_post_resolutions: based on Revenue Retained + Post Resolutions + + It is important to note that if we check the ongoing month, the count of + billable items and the corresponding share will be based on the projection, + rather than the actual figure. In this case, the MAE and MAPE of the projected + value are indicated in the model. + + While the growth and impact scores are computed at a monthly basis, their values + will update every day with the latest available projection. + + data_tests: + - dbt_utils.unique_combination_of_columns: + combination_of_columns: + - end_date + - id_deal + - dbt_utils.unique_combination_of_columns: + combination_of_columns: + - start_date + - id_deal + + columns: + - name: start_date + data_type: date + description: | + Start date of the period for which the revenue impact is computed. + Corresponds to the first day of the month. + data_tests: + - not_null + + - name: end_date + data_type: date + description: | + End date of the period for which the revenue impact is computed. + Corresponds to the last day of the month. + data_tests: + - not_null + + - name: id_deal + data_type: string + description: | + Unique ID for a deal, or account. + data_tests: + - not_null + + - name: deal + data_type: string + description: | + Concatenation of the deal ID and the deal name. + + - name: client_type + data_type: string + description: | + Type of the client, PLATFORM or API. + + - name: has_active_pms + data_type: boolean + description: | + Flag indicating if the deal has an active PMS or not. + + - name: active_pms_list + data_type: string + description: | + List of active PMS for the deal. It can be null if the deal has no + active PMS. + + - name: main_billing_country_iso_3_per_deal + data_type: string + description: | + Main billing country for the deal. It can be null. + + - name: deal_lifecycle_state + data_type: string + description: | + Lifecycle state of the deal. + + - name: deal_hubspot_stage + data_type: string + description: | + Hubspot stage of the deal. + + - name: account_manager + data_type: string + description: | + Account manager of the deal. It can be null. + + - name: live_date_utc + data_type: date + description: | + Live date of the deal according to HubSpot. It can be null. + + - name: cancellation_date_utc + data_type: date + description: | + Cancellation date of the deal according to HubSpot. It can be null. + + - name: growth_score + data_type: decimal + description: | + Growth score of the billable items, based on the average between: + - The billable items of a given month vs. the average of the previous + 3 months. + - The share a deal has in terms of billable items of a given month + if compared to the rest of the deals vs. the average of the previous 3 + months. + The growth score is capped between -1 and 1. + It can be overridden to -1 in case the deal is cancelled in the same month. + It cannot be null. + data_tests: + - not_null + - dbt_expectations.expect_column_values_to_be_between: + min_value: -1 + max_value: 1 + strictly: false + + - name: impact_score_total_revenue + data_type: decimal + description: | + Impact score of the growth score on the total revenue. + It is computed as the product of the growth score and the deal + contribution to the total revenue in the previous 12 months. + It cannot be null. + data_tests: + - not_null + - dbt_expectations.expect_column_values_to_be_between: + min_value: -1 + max_value: 1 + strictly: false + + - name: impact_score_revenue_retained_post_resolutions + data_type: decimal + description: | + Impact score of the growth score on the revenue retained post + resolutions. It is computed as the product of the growth score and + the deal contribution to the revenue retained post resolutions in + the previous 12 months. + It cannot be null. + data_tests: + - not_null + - dbt_expectations.expect_column_values_to_be_between: + min_value: -1 + max_value: 1 + strictly: false + + - name: categorisation_impact_score_revenue_retained_post_resolutions + data_type: string + description: | + Categorisation of the impact score on the revenue retained post + resolutions. It cannot be null. + data_tests: + - not_null + - accepted_values: + values: + - MAJOR DECLINE + - DECLINE + - FLAT + - GAIN + - MAJOR GAIN + + - name: rank_impact_score_total_revenue + data_type: integer + description: | + Monthly rank of the deal in terms of impact score on the total revenue. + + - name: rank_impact_score_revenue_retained_post_resolutions + data_type: integer + description: | + Monthly rank of the deal in terms of impact score on the revenue + retained post resolutions. + + - name: current_month_total_revenue_in_gbp + data_type: decimal + description: | + Total revenue in GBP for the current month. + If the month is in progress then this value will be null. + + - name: rolling_12_months_total_revenue_in_gbp + data_type: decimal + description: | + Total revenue in GBP for the previous 12 months. + It can be null. + + - name: share_total_revenue_rolling_12_months + data_type: decimal + description: | + Share of the deal in terms of total revenue in the previous 12 months. + It cannot be null. + data_tests: + - not_null + + - name: rank_total_revenue_rolling_12_months + data_type: integer + description: | + Monthly rank of the deal in terms of total revenue in the previous + 12 months. + + - name: current_month_revenue_retained_post_resolutions_in_gbp + data_type: decimal + description: | + Revenue retained post resolutions in GBP for the current month. + If the month is in progress then this value will be null. + + - name: rolling_12_months_revenue_retained_post_resolutions_in_gbp + data_type: decimal + description: | + Revenue retained post resolutions in GBP for the previous 12 months. + It can be null. + + - name: share_revenue_retained_post_resolutions_rolling_12_months + data_type: decimal + description: | + Share of the deal in terms of revenue retained post resolutions in + the previous 12 months. + It cannot be null. + data_tests: + - not_null + + - name: rank_revenue_retained_post_resolutions_rolling_12_months + data_type: integer + description: | + Monthly rank of the deal in terms of revenue retained post + resolutions in the previous 12 months. + + - name: current_month_billable_items + data_type: integer + description: | + Monthly billable items. If the month is in progress + then this value might be projected. + data_tests: + - dbt_expectations.expect_column_values_to_be_between: + min_value: 0 + strictly: false + + - name: share_billable_items_current_month + data_type: decimal + description: | + Share of the billable items for a given deal in the current month. + If the month is in progress then this value might be projected. + data_tests: + - dbt_expectations.expect_column_values_to_be_between: + min_value: 0 + strictly: false + + - name: rank_billable_items_current_month + data_type: integer + description: | + Monthly rank of the deal in terms of billable items in the current month. + If the month is in progress then this value might be projected. + + - name: projection_mean_absolute_error + data_type: decimal + description: | + Mean absolute error of the projection of the billable items. + It is null if the month is not in progress or value is projected + but there's no prior data to compare the projection against. + data_tests: + - dbt_expectations.expect_column_values_to_be_between: + min_value: 0 + strictly: false + + - name: projection_mean_absolute_percentage_error + data_type: decimal + description: | + Mean absolute percentage error of the projection of the billable items. + It is null if the month is not in progress or value is projected + but there's no prior data to compare the projection against. + data_tests: + - dbt_expectations.expect_column_values_to_be_between: + min_value: 0 + strictly: false + + - name: are_billable_items_projected + data_type: boolean + description: | + Flag indicating if the billable items are projected or not. + If the month is in progress then this value might be projected. + It can be null if there's no projection for that deal. + + - name: is_growth_score_overridden_due_to_cancellation + data_type: boolean + description: | + Flag indicating if the growth score is overridden to -1 due to + cancellation in the same month.