Retention and engagement

Modeling upgrades, downgrades and renewals as daily movements

How many members upgrade, renew or lapse each day, by site and acquisition source?

Short answer

Trust: Inferred

Treat each movement as a dated event, count it on the day it happens at one grain (here site, source and day), and check every branch counts the population you mean, once. In this project the downgrade branch joins to every upgrade a member made, so one lapse can count three times and members who never upgraded are missed, and there is no active count to turn movements into a churn rate.

Decision it supports: Where retention effort and renewal reminders should go.

What to measure

Leads

Trust: Source-defined

Users created that day.

Defined as churn.leads

SUM(churn.leads)Trust: Translated

Show the SQL: Leads
Redshift SQLTrust: Translated
SUM(churn.leads)

Translated to Redshift SQL from membership_activity.view.lookml L83-86. Not run against a warehouse.

Show the original LookML: membership_activity.view.lookml L83-86
dating_platform/membership_activity.view.lookmlL83-86
    sql: ${TABLE}.site  - dimension: source    sql: ${TABLE}.source  - measure: leads    type: sum    sql: ${TABLE}.leads    detail: detail*  - measure: downgrades    type: sum    sql: ${TABLE}.downgrades

dating-platform, membership_activity.view.lookml, lines 83-86

Upgrades

Trust: Source-defined

Upgrade transactions that day.

Defined as churn.upgrades

SUM(churn.upgrades)Trust: Translated

Show the SQL: Upgrades
Redshift SQLTrust: Translated
SUM(churn.upgrades)

Translated to Redshift SQL from membership_activity.view.lookml L95-98. Not run against a warehouse.

Show the original LookML: membership_activity.view.lookml L95-98
dating_platform/membership_activity.view.lookmlL95-98
    type: sum    sql: ${TABLE}.renewals    detail: detail*  - measure: upgrades    type: sum    sql: ${TABLE}.upgrades    detail: detail*  sets:    detail:    - id

dating-platform, membership_activity.view.lookml, lines 95-98

Downgrades

Trust: Source-defined

Lapsed subscriptions with auto-renew off, dated by renewal date, and only for members with an upgrade transaction: members who never upgraded are not counted.

Defined as churn.downgrades

SUM(churn.downgrades)Trust: Translated

Show the SQL: Downgrades
Redshift SQLTrust: Translated
SUM(churn.downgrades)

Translated to Redshift SQL from membership_activity.view.lookml L87-90. Not run against a warehouse.

Show the original LookML: membership_activity.view.lookml L87-90
dating_platform/membership_activity.view.lookmlL87-90
    type: sum    sql: ${TABLE}.leads    detail: detail*  - measure: downgrades    type: sum    sql: ${TABLE}.downgrades    detail: detail*  - measure: renewals    type: sum    sql: ${TABLE}.renewals

dating-platform, membership_activity.view.lookml, lines 87-90

Renewals

Trust: Source-defined

Renewal transactions that day.

Defined as churn.renewals

SUM(churn.renewals)Trust: Translated

Show the SQL: Renewals
Redshift SQLTrust: Translated
SUM(churn.renewals)

Translated to Redshift SQL from membership_activity.view.lookml L91-94. Not run against a warehouse.

Show the original LookML: membership_activity.view.lookml L91-94
dating_platform/membership_activity.view.lookmlL91-94
    type: sum    sql: ${TABLE}.downgrades    detail: detail*  - measure: renewals    type: sum    sql: ${TABLE}.renewals    detail: detail*  - measure: upgrades    type: sum    sql: ${TABLE}.upgrades

dating-platform, membership_activity.view.lookml, lines 91-94

Active subscription

Trust: Source-defined

Active means the next renewal date is in the future. The status column may disagree.

Defined as subscriptions.currently_active

CASE WHEN subscriptions.next_renewal_on > GETDATE() THEN 'yes' ELSE 'no' ENDTrust: Translated

Show the SQL: Active subscription
Redshift SQLTrust: Translated
CASE WHEN subscriptions.next_renewal_on > GETDATE() THEN 'yes' ELSE 'no' END

Translated to Redshift SQL from memberships.view.lookml L36-38. Not run against a warehouse.

Show the original LookML: memberships.view.lookml L36-38
dating_platform/memberships.view.lookmlL36-38
  - dimension: trial_days    type: int    sql: ${TABLE}.trial_days  - dimension: currently_active    type: yesno    sql: ${TABLE}.next_renewal_on > GETDATE()  - dimension_group: updated    type: time    timeframes:

dating-platform, memberships.view.lookml, lines 36-38

Churn rate

Trust: Missing

The explore is labelled Churn Report but defines no rate. It needs active subscriptions as a denominator, which the view does not have.

The query

Movements by month

Trust: Suggested

Leads, upgrades, downgrades and renewals per month, read from the daily movements table. No dashboard tile reads this explore, so the field combination is ours.

Show the SQL: movements by month
Redshift SQLTrust: Suggested
SELECT  DATE_TRUNC('month', churn.calendar_date) AS month,  SUM(churn.leads) AS leads,  SUM(churn.upgrades) AS upgrades,  SUM(churn.downgrades) AS downgrades,  SUM(churn.renewals) AS renewalsFROM churn  -- derived table, defined in the sourceGROUP BY 1ORDER BY month

Suggested query. Fields translated to Redshift SQL from enterprise.model.lookml L5-7 and membership_activity.view.lookml L76-78, L83-98. Not run against a warehouse.

The data behind it

Trust: Inferred
Entities
user, subscription, user transaction
Grain
churn: one row per site, source and calendar day.
Time
day and month. Each movement is dated by a different column.
Key fields
churn.sitechurn.sourcechurn.daysubscriptions.auto_renewsubscriptions.next_renewalsubscriptions.status

What can change the answer 3

Caution: Downgrades fan out by upgrade count

Trust: Inferred

The downgrade branch joins each subscription to every upgrade transaction of the user.

What goes wrong: A member with three past upgrades counts as three downgrades.

Read the guide: Join fanout

Source: membership_activity.view.lookml L22-38
dating_platform/membership_activity.view.lookmlL22-38
                 U.source,                 trunc(U.created_at)        UNION ALL        SELECT    U.site,                  U.source,                  TRUNC(S.next_renewal_on) AS calendar_date,                  0,                  COUNT(1) AS downgrades,                  0,                  0 AS renewals        FROM  subscriptions S              JOIN users U ON S.user_id = U.id              JOIN user_transactions T              ON  S.user_id = T.user_id                  AND T.type = 'upgrade'        WHERE S.auto_renew = false              AND S.next_renewal_on < GETDATE()        GROUP BY U.site,                 U.source,                 trunc(S.next_renewal_on)        UNION ALL        SELECT  U.site,                U.source,

dating-platform, membership_activity.view.lookml, lines 22-38

Caution: Downgrades are dated by renewal date

Trust: Inferred

A downgrade counts on next_renewal_on once that date has passed. It measures lapses, not the day a member turned off auto-renew.

Source: membership_activity.view.lookml L22-35
dating_platform/membership_activity.view.lookmlL22-35
                 U.source,                 trunc(U.created_at)        UNION ALL        SELECT    U.site,                  U.source,                  TRUNC(S.next_renewal_on) AS calendar_date,                  0,                  COUNT(1) AS downgrades,                  0,                  0 AS renewals        FROM  subscriptions S              JOIN users U ON S.user_id = U.id              JOIN user_transactions T              ON  S.user_id = T.user_id                  AND T.type = 'upgrade'        WHERE S.auto_renew = false              AND S.next_renewal_on < GETDATE()        GROUP BY U.site,                 U.source,                 trunc(S.next_renewal_on)

dating-platform, membership_activity.view.lookml, lines 22-35

Note: Two definitions of active

Trust: Inferred

currently_active uses the renewal date while status holds the system state. They can disagree for paused or refunded subscriptions.

Show the SQL: currently active
Redshift SQLTrust: Translated
CASE WHEN subscriptions.next_renewal_on > GETDATE() THEN 'yes' ELSE 'no' END

Translated to Redshift SQL from memberships.view.lookml L36-38. Not run against a warehouse.

Show the original LookML: memberships.view.lookml L36-38
dating_platform/memberships.view.lookmlL36-38
  - dimension: trial_days    type: int    sql: ${TABLE}.trial_days  - dimension: currently_active    type: yesno    sql: ${TABLE}.next_renewal_on > GETDATE()  - dimension_group: updated    type: time    timeframes:

dating-platform, memberships.view.lookml, lines 36-38

How the real implementation does it

churn

Trust: Source-defined

Leads, downgrades, upgrades and renewals are stacked with UNION ALL and summed per site, source and day.

Source: membership_activity.view.lookml L4-20
dating_platform/membership_activity.view.lookmlL4-20
- view: churn  derived_table:    sql: |      SELECT  site, source, calendar_date,              SUM(upgrades) AS upgrades,              SUM(downgrades) AS downgrades,              SUM(leads) AS leads,              SUM(renewals) AS renewals      FROM  (        SELECT  U.site,                 U.source,                 TRUNC(U.created_at) AS calendar_date,                 COUNT(1) AS leads,                 0 AS downgrades,                 0 AS upgrades,                 0 AS renewals          FROM  users U          GROUP BY U.site,                 U.source,                 trunc(U.created_at)        UNION ALL        SELECT    U.site,                  U.source,

dating-platform, membership_activity.view.lookml, lines 4-20

churn

Trust: Source-defined

The downgrade branch finds subscriptions with auto-renew off whose renewal date has passed, joined to the member's upgrade transactions, so members who never upgraded drop out.

Source: membership_activity.view.lookml L22-38
dating_platform/membership_activity.view.lookmlL22-38
                 U.source,                 trunc(U.created_at)        UNION ALL        SELECT    U.site,                  U.source,                  TRUNC(S.next_renewal_on) AS calendar_date,                  0,                  COUNT(1) AS downgrades,                  0,                  0 AS renewals        FROM  subscriptions S              JOIN users U ON S.user_id = U.id              JOIN user_transactions T              ON  S.user_id = T.user_id                  AND T.type = 'upgrade'        WHERE S.auto_renew = false              AND S.next_renewal_on < GETDATE()        GROUP BY U.site,                 U.source,                 trunc(S.next_renewal_on)        UNION ALL        SELECT  U.site,                U.source,

dating-platform, membership_activity.view.lookml, lines 22-38

subscriptions

Trust: Source-defined

Active is computed from the renewal date, not the status column.

Show the SQL: active subscription
Redshift SQLTrust: Translated
CASE WHEN subscriptions.next_renewal_on > GETDATE() THEN 'yes' ELSE 'no' END

Translated to Redshift SQL from memberships.view.lookml L36-38. Not run against a warehouse.

Show the original LookML: memberships.view.lookml L36-38
dating_platform/memberships.view.lookmlL36-38
  - dimension: trial_days    type: int    sql: ${TABLE}.trial_days  - dimension: currently_active    type: yesno    sql: ${TABLE}.next_renewal_on > GETDATE()  - dimension_group: updated    type: time    timeframes:

dating-platform, memberships.view.lookml, lines 36-38

Other ways to measure it

Count each lapse once

Trust: Suggested

Use EXISTS against upgrades instead of a join, so each lapsed subscription counts once.

Show the suggested SQL: churn downgrades branch
Suggested, not from source
SELECT U.site, U.source, TRUNC(S.next_renewal_on) AS calendar_date,       COUNT(DISTINCT S.id) AS downgradesFROM subscriptions SJOIN users U ON S.user_id = U.idWHERE S.auto_renew = false  AND S.next_renewal_on < GETDATE()  AND EXISTS (SELECT 1 FROM user_transactions T              WHERE T.user_id = S.user_id AND T.type = 'upgrade')GROUP BY 1, 2, 3

Add a daily active snapshot

Trust: Suggested

Store a daily count of active subscriptions, so churn rate has a denominator. Rebuilding history from today's renewal dates, as below, only approximates the past, because renewals overwrite them; start a daily snapshot and keep each day's result.

Show the suggested SQL: daily active snapshot
Suggested, not from source
-- calendar: one row per day, which you supplySELECT  D.calendar_date,  U.site,  U.source,  COUNT(DISTINCT S.id) AS active_subscriptionsFROM calendar DJOIN subscriptions S  ON S.created_at <= D.calendar_date AND S.next_renewal_on > D.calendar_dateJOIN users U ON S.user_id = U.idGROUP BY 1, 2, 3