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: InferredTreat 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-definedUsers created that day.
Defined as churn.leads
SUM(churn.leads)Trust: Translated
Show the SQL: Leads
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
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-definedUpgrade transactions that day.
Defined as churn.upgrades
SUM(churn.upgrades)Trust: Translated
Show the SQL: Upgrades
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
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-definedLapsed 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
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
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-definedRenewal transactions that day.
Defined as churn.renewals
SUM(churn.renewals)Trust: Translated
Show the SQL: Renewals
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
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-definedActive 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
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
- 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: MissingThe 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: SuggestedLeads, 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
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
dayandmonth. 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: InferredThe 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.
Source: membership_activity.view.lookml L22-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: InferredA 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
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: Inferredcurrently_active uses the renewal date while status holds the system state. They can disagree for paused or refunded subscriptions.
Show the SQL: currently active
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
- 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-definedLeads, downgrades, upgrades and renewals are stacked with UNION ALL and summed per site, source and day.
Source: membership_activity.view.lookml L4-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-definedThe 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
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-definedActive is computed from the renewal date, not the status column.
Show the SQL: active subscription
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
- 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: SuggestedUse EXISTS against upgrades instead of a join, so each lapsed subscription counts once.
Show the suggested SQL: churn downgrades branch
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: SuggestedStore 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
-- 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
