Retention Challenges

Ratio Metrics & Churn Users

Đặt Vấn Đề

  • Klipfolio, một công ty SaaS cho phép users tạo bảng điều khiển (dashboard) trực tuyến.
  • Chỉ số đo lường được áp dụng trong case này: (Số lượt edit dashboard) / (Số lượt save dashboard) => Được gọi là tỷ lệ kém hiệu quả (Inefficiency Rate).
  • Những users có chỉ số này cao (tức là edit rất nhiều nhưng save lại ít) có nguy cơ churn cao hơn.
  • Việc phải edit nhiều lần mới đạt được kết quả mong muốn và save lại cho thấy users có thể đang gặp khó khăn, tốn nhiều công sức hơn để sử dụng sản phẩm. Sự không hiệu quả này dẫn đến trải nghiệm không tốt và làm tăng nguy cơ churn của users.

Thu Thập Dữ Liệu

  • Table: users
    • user_id, signup_date, last_seen_date, churn_date, is_churned
  • Table: dashboard_actions
    • action_id, user_id, dashboard_id, action_type, action_timestamp

Phân Tích & Khám Phá Insight Ẩn

  • Challenge 01: Tính edits_per_save cho mỗi user.
    • Thước đo cơ bản, định lượng được mức độ “kém hiệu quả” của từng user riêng lẻ khi họ sử dụng sản phẩm. Đây là bước đầu tiên để xác định ai có thể đang gặp khó khăn.
declare @end_date datetime = getdate();
declare @start_date datetime = dateadd(day, -90, @end_date);
with user_action_counts as (
    select
        user_id,
        sum(case when action_type = 'edit' then 1 else 0 end) as total_edits,
        sum(case when action_type = 'save' then 1 else 0 end) as total_saves
    from dashboard_actions
    where action_timestamp between @start_date and @end_date
    group by user_id
)
select
    u.user_id,
    u.is_churned,
    isnull(ua.total_edits, 0) as total_edits_last_90d,
    isnull(ua.total_saves, 0) as total_saves_last_90d,
    case
        when isnull(ua.total_saves, 0) > 0 then
            cast(isnull(ua.total_edits, 0) as float) / ua.total_saves
        else
            case when isnull(ua.total_edits, 0) > 0 then 9999.0 else 0.0 end
    end as edits_per_save_ratio
from users u
left join user_action_counts ua on u.user_id = ua.user_id
where isnull(ua.total_edits, 0) > 0 or isnull(ua.total_saves, 0) > 0
order by edits_per_save_ratio desc;
  • Challenge 02: Phân tích tương quan giữa nhóm tỷ lệ và churn.
    • Kiểm chứng giả thuyết cốt lõi: Liệu việc sử dụng sản phẩm kém hiệu quả (tỷ lệ edits_per_save cao) có thực sự liên quan đến việc user rời bỏ sản phẩm nhiều hơn không?
with user_total_actions as (
    select
        user_id,
        sum(case when action_type = 'edit' then 1 else 0 end) as total_edits,
        sum(case when action_type = 'save' then 1 else 0 end) as total_saves
    from dashboard_actions
    -- where action_timestamp > 'yyyy-mm-dd' -- co the gioi han thoi gian neu can
    group by user_id
), user_ratios as (
    select
        uta.user_id,
        uta.total_edits,
        uta.total_saves,
        case
            when uta.total_saves > 0 then cast(uta.total_edits as float) / uta.total_saves
            else case when uta.total_edits > 0 then 9999.0 else 0.0 end
        end as edits_per_save_ratio
    from user_total_actions uta
    where uta.total_edits > 0 or uta.total_saves > 0 
), user_ratio_groups as (
    select
        ur.user_id,
        ur.edits_per_save_ratio,
        case
            when ur.edits_per_save_ratio < 3.0 then '1_low (<3)'
            when ur.edits_per_save_ratio >= 3.0 and ur.edits_per_save_ratio < 8.0 then '2_medium (3-8)'
            when ur.edits_per_save_ratio >= 8.0 then '3_high (>=8)'
            else '0_no_activity_or_ratio'
        end as efficiency_group
    from user_ratios ur
)
select
    urg.efficiency_group,
    count(distinct u.user_id) as total_users_in_group,
    sum(case when u.is_churned = 1 then 1 else 0 end) as churned_users_in_group,
    cast(sum(case when u.is_churned = 1 then 1 else 0 end) as float) * 100.0 / count(distinct u.user_id) as churn_rate_percent
from user_ratio_groups urg
join users u on urg.user_id = u.user_id
group by urg.efficiency_group
order by urg.efficiency_group;
  • Challenge 3: Phân tích xu hướng tỷ lệ trước khi churn.
    • Khám phá xem sự kém hiệu quả có phải là một dấu hiệu cảnh báo sớm hay không. Liệu user có xu hướng “vật lộn” nhiều hơn ngay trước khi họ quyết định rời đi? Nếu có, chúng ta có thể phát hiện nguy cơ churn sớm hơn để can thiệp.
with monthly_actions as (
    select
        user_id,
        datefromparts(year(action_timestamp), month(action_timestamp), 1) as action_month,
        sum(case when action_type = 'edit' then 1 else 0 end) as monthly_edits,
        sum(case when action_type = 'save' then 1 else 0 end) as monthly_saves
    from dashboard_actions
    group by
        user_id,
        datefromparts(year(action_timestamp), month(action_timestamp), 1)
), monthly_ratios as (
    select
        ma.user_id,
        ma.action_month,
        ma.monthly_edits,
        ma.monthly_saves,
        case
            when ma.monthly_saves > 0 then cast(ma.monthly_edits as float) / ma.monthly_saves
            else case when ma.monthly_edits > 0 then 9999.0 else 0.0 end
        end as monthly_edits_per_save_ratio
    from monthly_actions ma
    where ma.monthly_edits > 0 or ma.monthly_saves > 0
), churned_users_months_before_churn as (
    select
        mr.user_id,
        u.churn_date,
        mr.action_month,
        mr.monthly_edits_per_save_ratio,
        datediff(month, mr.action_month, u.churn_date) as months_before_churn
    from monthly_ratios mr
    join users u on mr.user_id = u.user_id
    where u.is_churned = 1 and u.churn_date is not null
      and mr.action_month <= u.churn_date 
)
select
    cumbc.months_before_churn,
    avg(cumbc.monthly_edits_per_save_ratio) as avg_ratio_before_churn,
    count(distinct cumbc.user_id) as num_users_in_group
from churned_users_months_before_churn cumbc
where cumbc.months_before_churn between 0 and 6 
group by cumbc.months_before_churn
order by cumbc.months_before_churn asc; 
  • Challenge 4: Phân tích tương tác giữa tỷ lệ và thời gian sử dụng.
    • Liệu sự kém hiệu quả có tác động khác nhau đến new users (giai đoạn onboarding quan trọng) so với users đã gắn bó lâu năm hay không. Điều này giúp tối ưu hóa trải nghiệm cho từng giai đoạn vòng đời khách hàng.
with user_total_actions as (
    select
        user_id,
        sum(case when action_type = 'edit' then 1 else 0 end) as total_edits,
        sum(case when action_type = 'save' then 1 else 0 end) as total_saves
    from dashboard_actions
    group by user_id
), user_ratios as (
    select
        uta.user_id,
        case
            when uta.total_saves > 0 then cast(uta.total_edits as float) / uta.total_saves
            else case when uta.total_edits > 0 then 9999.0 else 0.0 end
        end as edits_per_save_ratio
    from user_total_actions uta
    where uta.total_edits > 0 or uta.total_saves > 0
),
user_tenure_groups as (
    select
        u.user_id,
        u.is_churned,
        datediff(day, u.signup_date, isnull(u.churn_date, u.last_seen_date)) as tenure_days,
        case
            when datediff(day, u.signup_date, isnull(u.churn_date, u.last_seen_date)) < 60 then '1_new (<60d)'
            when datediff(day, u.signup_date, isnull(u.churn_date, u.last_seen_date)) >= 60
             and datediff(day, u.signup_date, isnull(u.churn_date, u.last_seen_date)) < 180 then '2_medium (60-180d)'
            else '3_tenured (>=180d)'
        end as tenure_group
    from users u
),
user_efficiency_groups as (
    select
        ur.user_id,
        case
             when ur.edits_per_save_ratio < 3.0 then 'low_efficiency'
             when ur.edits_per_save_ratio >= 3.0 and ur.edits_per_save_ratio < 8.0 then 'medium_efficiency'
             when ur.edits_per_save_ratio >= 8.0 then 'high_efficiency'
             else 'zero_ratio'
        end as efficiency_group
    from user_ratios ur
)
select
    utg.tenure_group,
    ueg.efficiency_group,
    count(distinct utg.user_id) as total_users,
    sum(case when utg.is_churned = 1 then 1 else 0 end) as churned_users,
    cast(sum(case when utg.is_churned = 1 then 1 else 0 end) as float) * 100.0 / count(distinct utg.user_id) as churn_rate_percent
from user_tenure_groups utg
join user_efficiency_groups ueg on utg.user_id = ueg.user_id
group by
    utg.tenure_group,
    ueg.efficiency_group
order by
    utg.tenure_group,
    ueg.efficiency_group;
  • Challenge 05: Phân tích kết hợp tỷ lệ và tần suất hoạt động
    • Liệu người dùng kém hiệu quả là người rất tích cực nhưng liên tục gặp khó khăn hay là người chỉ thử một chút rồi bỏ cuộc? Hiểu điều này giúp phân loại mức độ rủi ro và chiến lược can thiệp phù hợp hơn.
declare @end_date datetime = getdate();
declare @start_date datetime = dateadd(day, -90, @end_date);
with user_actions_last_90d as (
    select
        user_id,
        sum(case when action_type = 'edit' then 1 else 0 end) as total_edits,
        sum(case when action_type = 'save' then 1 else 0 end) as total_saves,
        count(*) as total_actions 
    from dashboard_actions
    where action_timestamp between @start_date and @end_date
    group by user_id
), user_metrics_last_90d as (
    select
        ua.user_id,
        ua.total_actions,
        case
            when ua.total_saves > 0 then cast(ua.total_edits as float) / ua.total_saves
            else case when ua.total_edits > 0 then 9999.0 else 0.0 end
        end as edits_per_save_ratio
    from user_actions_last_90d ua
    where ua.total_actions > 0 
),
user_segments as (
    select
        um.user_id,
        um.edits_per_save_ratio,
        um.total_actions,
        case
            when um.edits_per_save_ratio >= 8.0 then 'high_efficiency_ratio'
            else 'low_medium_efficiency_ratio' 
        end as efficiency_segment,
        case
            when um.total_actions >= 50 then 'high_activity' 
            else 'low_activity'
        end as activity_segment
    from user_metrics_last_90d um
)
select
    us.efficiency_segment,
    us.activity_segment,
    count(distinct u.user_id) as total_users,
    sum(case when u.is_churned = 1 then 1 else 0 end) as churned_users,
    case
        when count(distinct u.user_id) > 0 then
            cast(sum(case when u.is_churned = 1 then 1 else 0 end) as float) * 100.0 / count(distinct u.user_id)
        else 0.0
    end as churn_rate_percent
from user_segments us
join users u on us.user_id = u.user_id
group by
    us.efficiency_segment,
    us.activity_segment
order by
    us.efficiency_segment,
    us.activity_segment;
  • Challenge 06: Phân tích chuỗi EDIT liên tiếp kéo dài.
    • Phát hiện những khoảnh khắc “bế tắc” của users (thể hiện qua việc edit liên tục không ngừng nghỉ mà không save). Những khoảnh khắc này có thể là tín hiệu dự báo churn tức thời và mạnh mẽ hơn so với tỷ lệ trung bình.
with action_sequences as (
    select
        action_id,
        user_id,
        dashboard_id,
        action_type,
        action_timestamp,
        case
            when lag(action_type, 1, action_type) over(partition by user_id, dashboard_id order by action_timestamp) = action_type
            then 0 
            else 1
        end as is_new_sequence_start
    from dashboard_actions
),
sequence_groups as (
    select
        aqs.*,
        sum(aqs.is_new_sequence_start) over(partition by user_id, dashboard_id order by action_timestamp rows unbounded preceding) as sequence_group_id
    from action_sequences aqs
),
edit_streaks as (
    select
        user_id,
        dashboard_id,
        sequence_group_id,
        min(action_timestamp) as streak_start_time,
        max(action_timestamp) as streak_end_time,
        count(*) as streak_length 
    from sequence_groups
    where action_type = 'edit'
    group by user_id, dashboard_id, sequence_group_id
),
users_with_long_streaks as (
    select distinct user_id
    from edit_streaks
    where streak_length >= 10 
)
select
    case
        when uwls.user_id is not null
        then 'experienced long edit streak (>=10)'
        else 'no long edit streak'
    end as user_streak_experience,
    count(distinct u.user_id) as total_users,
    sum(case when u.is_churned = 1 then 1 else 0 end) as churned_users,
    case
        when count(distinct u.user_id) > 0 then
           cast(sum(case when u.is_churned = 1 then 1 else 0 end) as float) * 100.0 / count(distinct u.user_id)
        else 0.0
    end as churn_rate_percent
from users u
left join users_with_long_streaks uwls on u.user_id = uwls.user_id
group by
    case
        when uwls.user_id is not null
        then 'experienced long edit streak (>=10)'
        else 'no long edit streak'
    end;
  • Challenge 07: Phân tích tác động của “Lộ trình Hiệu quả” đến tỷ lệ churn dài hạn.
    • Đánh giá xem việc users thay đổi mức độ hiệu quả khi sử dụng sản phẩm theo thời gian (ví dụ: từ kém hiệu quả trở nên hiệu quả hơn, hoặc ngược lại) có thực sự ảnh hưởng đến khả năng họ gắn bó lâu dài hay không.
declare @period1_start_month int = 1; 
declare @period1_end_month int = 2;   
declare @period2_start_month int = 3; 
declare @period2_end_month int = 4; 
with actions_period1 as (
    select
        da.user_id,
        sum(case when da.action_type = 'edit' then 1 else 0 end) as edits_p1,
        sum(case when da.action_type = 'save' then 1 else 0 end) as saves_p1
    from dashboard_actions da
    join users u on da.user_id = u.user_id
    where da.action_timestamp >= dateadd(month, @period1_start_month - 1, u.signup_date)
      and da.action_timestamp < dateadd(month, @period1_end_month, u.signup_date)
    group by da.user_id
), ratio_period1 as (
    select
        user_id,
        edits_p1,
        saves_p1,
        case
            when saves_p1 > 0 then cast(edits_p1 as float) / saves_p1
            else case when edits_p1 > 0 then 9999.0 else 0.0 end
        end as ratio_p1
    from actions_period1
    where edits_p1 > 0 or saves_p1 > 0 
),
actions_period2 as (
     select
        da.user_id,
        sum(case when da.action_type = 'edit' then 1 else 0 end) as edits_p2,
        sum(case when da.action_type = 'save' then 1 else 0 end) as saves_p2
    from dashboard_actions da
    join users u on da.user_id = u.user_id
    where da.action_timestamp >= dateadd(month, @period2_start_month - 1, u.signup_date)
      and da.action_timestamp < dateadd(month, @period2_end_month, u.signup_date)
      and u.signup_date <= dateadd(month, -@period2_start_month, getdate())
    group by da.user_id
), ratio_period2 as (
    select
        user_id,
        edits_p2,
        saves_p2,
        case
            when saves_p2 > 0 then cast(edits_p2 as float) / saves_p2
            else case when edits_p2 > 0 then 9999.0 else 0.0 end
        end as ratio_p2
    from actions_period2
    where edits_p2 > 0 or saves_p2 > 0 
),
categorized_ratios as (
    select
        u.user_id,
        u.is_churned,
        case
            when rp1.ratio_p1 < 3.0 then 'low'
            when rp1.ratio_p1 >= 3.0 and rp1.ratio_p1 < 8.0 then 'medium'
            when rp1.ratio_p1 >= 8.0 then 'high'
            else 'inactive_p1' 
        end as category_p1,
        case
            when rp2.ratio_p2 < 3.0 then 'low'
            when rp2.ratio_p2 >= 3.0 and rp2.ratio_p2 < 8.0 then 'medium'
            when rp2.ratio_p2 >= 8.0 then 'high'
            else 'inactive_p2' 
        end as category_p2
    from users u
    left join ratio_period1 rp1 on u.user_id = rp1.user_id
    left join ratio_period2 rp2 on u.user_id = rp2.user_id
    where u.signup_date <= dateadd(month, -@period2_end_month, getdate())
      and rp1.user_id is not null 
      and rp2.user_id is not null 
),
trajectory_segments as (
    select
        user_id,
        is_churned,
        category_p1,
        category_p2,
        case
            when category_p1 = 'high' and category_p2 = 'low' then 'improved (h->l)'
            when category_p1 = 'high' and category_p2 = 'medium' then 'improved (h->m)'
            when category_p1 = 'medium' and category_p2 = 'low' then 'improved (m->l)'
            when category_p1 = 'low' and category_p2 = 'high' then 'worsened (l->h)'
            when category_p1 = 'low' and category_p2 = 'medium' then 'worsened (l->m)'
            when category_p1 = 'medium' and category_p2 = 'high' then 'worsened (m->h)'
            when category_p1 = 'low' and category_p2 = 'low' then 'consistent_low'
            when category_p1 = 'medium' and category_p2 = 'medium' then 'consistent_medium'
            when category_p1 = 'high' and category_p2 = 'high' then 'consistent_high'
            else 'other_transition' 
        end as efficiency_trajectory
    from categorized_ratios
    where category_p1 not like 'inactive%' and category_p2 not like 'inactive%'
)
select
    ts.efficiency_trajectory,
    count(ts.user_id) as total_users,
    sum(case when ts.is_churned = 1 then 1 else 0 end) as churned_users,
    case
        when count(ts.user_id) > 0 then
            cast(sum(case when ts.is_churned = 1 then 1 else 0 end) as float) * 100.0 / count(ts.user_id)
        else 0.0
    end as churn_rate_percent
from trajectory_segments ts
group by ts.efficiency_trajectory
order by churn_rate_percent desc; 

Leave a Comment