Shipping data model - survival analysis

This commit is contained in:
CIEF ACC1
2023-12-29 09:43:22 +00:00
parent a4c4fc83cb
commit 1b60be4522
@@ -0,0 +1,183 @@
-- Survival analysis code reference from https://www.crosstab.io/articles/sql-survival-curves/
-- AVAILABLE FILTER VALUE
-- Source:preset_custom_filter Column:day_use_to_churn
{% set day_use_to_churn = 120 %}
-- IMPORT
WITH companies AS (
SELECT * FROM {{ ref('dim_shipping__companies') }}
),
origin_warehouse_shipping_packing_lists AS (
SELECT * FROM {{ ref('fct_shipping__origin_warehouse_shipping_packing_lists') }}
),
-- LOGIC
duration_table AS (
SELECT
companies.sub_company_id,
companies.sub_company_marking_id,
companies.sub_company_created_datetime,
COUNT(origin_warehouse_shipping_packing_lists.origin_warehouse_shipping_packing_list_id) AS count_order,
COUNT(IFF(
origin_warehouse_shipping_packing_lists.origin_warehouse_shipping_order_status IN ('APPROVED', 'COMPLETED'),
origin_warehouse_shipping_packing_lists.origin_warehouse_shipping_packing_list_id,
null
)) AS count_completed_order,
MAX(origin_warehouse_shipping_packing_lists.origin_warehouse_shipping_order_created_datetime) AS last_order_datetime,
COALESCE(last_order_datetime, sub_company_created_datetime) AS last_activity_datetime,
IFF(last_order_datetime IS NULL, 1, 0) AS is_never_order_company,
-- if is_churn_company = 0, the data will be censored
IFF(
DATEDIFF(day, last_activity_datetime, CURRENT_DATE()) >= {{day_use_to_churn}},
1,
0
) AS is_churn_company,
CASE
WHEN is_churn_company = 1 THEN
DATEDIFF(day, companies.sub_company_created_datetime, last_activity_datetime) + {{day_use_to_churn}}
ELSE
DATEDIFF(day, companies.sub_company_created_datetime, CURRENT_DATE())
END AS survival_time_days --event duration
FROM companies
LEFT JOIN origin_warehouse_shipping_packing_lists
ON (companies.sub_company_id = origin_warehouse_shipping_packing_lists.sub_company_id)
GROUP BY
companies.sub_company_id,
companies.sub_company_marking_id,
companies.sub_company_created_datetime
HAVING
-- companies without order will not be relevant to the analysis
is_never_order_company = 0
),
-- the daily_tally cte count the total number of observations at each survival_time_day
-- and the number of company that have churned at that survival_time_day
daily_observation_tally AS (
SELECT
survival_time_days,
COUNT(survival_time_days) AS total_number_of_observations,
SUM(is_churn_company) AS events -- considering only churned company
FROM duration_table
GROUP BY survival_time_days
ORDER BY survival_time_days
),
-- the cumulative_tally cte counts the number of subjects still at risk of experiencing churn
cumulative_tally AS (
SELECT
survival_time_days,
events,
total_number_of_observations,
( SELECT COUNT(DISTINCT(sub_company_id)) FROM duration_table ) AS total_number_of_subjects,
-- cumulative sum of observations at all previous survival_time_days SUBTRACTED by total_number_of_subjects
total_number_of_subjects - COALESCE(
SUM(total_number_of_observations) OVER (ORDER BY survival_time_days ROWS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING)
,0
) AS at_risk
FROM daily_observation_tally
),
-- At each survival_time_day, count number of censored subject
-- censored subjects = # subject at risk - churned - # subject at risk in the next duration
compute_censored_subjects AS (
SELECT
total_number_of_subjects,
survival_time_days,
at_risk,
total_number_of_observations,
events,
at_risk - events - COALESCE(
LEAD(at_risk, 1) OVER (ORDER BY survival_time_days)
,0
) AS censored
FROM cumulative_tally
-- Simply subtracting events from number of observations would incorrectly ignore subjects censored at durations that are dropped from the output table
WHERE events > 0
),
compute_probability AS (
SELECT
*,
-- The survival probability represents the probability of customers that will not churn up to a specific tenure
-- Example: survival_day = 96, survival_prob = 95%. For customers with 96 days of tenure, the customer has a 95% chance of not churning.
EXP(SUM(
-- When events / at_risk = 1, we replace value 1 with a value close to 1, to prevent log 0 which causes infinity value
LN(CASE WHEN (1 - events / at_risk) = 0 THEN 0.999999 ELSE (1 - events / at_risk) END)
) OVER (
ORDER BY survival_time_days ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
)
) AS survival_probability,
100 * (1 - EXP(SUM(
-- When events / at_risk = 1, we replace value 1 with a value close to 1, to prevent log 0 which causes infinity value
LN(CASE WHEN (1 - events / at_risk) = 0 THEN 0.999999 ELSE (1 - events / at_risk) END)
) OVER (
ORDER BY survival_time_days ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
)
)) AS conversion_percentage,
SUM(events / at_risk)
OVER (
ORDER BY survival_time_days ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) AS cumulative_hazard,
'{{ modules.datetime.datetime.now(modules.pytz.timezone("Asia/Kuala_Lumpur")) }}' AS _dbt_ran_datetime
FROM compute_censored_subjects
),
-- FINAL
final__rep_shipping__survival_analysis AS (
SELECT
-- dimension
survival_time_days,
at_risk,
total_number_of_observations,
events,
censored,
-- measures
survival_probability,
conversion_percentage,
cumulative_hazard,
-- metadata
_dbt_ran_datetime
FROM compute_probability
)
SELECT * FROM final__rep_shipping__survival_analysis