Involuntary churn is a subset of churn in which the cancellation event was not directly initiated by the customer. For us, this occurs when there have been four consecutive failed payments.
The reasons for the failed payments are varied, but we can assume that there are customers whose intention was to continue subscribing to the service. In most cases, they could not because their credit card either expired or was declined for one reason or another.
In this analysis we’ll start small. We’ll try to answer three simple questions about involuntary churn to gain a better understanding of where we stand:
- What is the ratio (%) of involuntary churn to total churn ?
- How much revenue is lost to involuntary churn?
- We send 3 reminder emails after a failed payment - how many people open them? How many open them on a mobile device?
Data collection
To answer the first question, we can query the revenue_events
table, which records MRR-related events like churn, upgrades, downgrades, and the like. We can join in the stripe._charges
table to check whether the charges associated with the last invoice were paid or failed.
with last_invoice as (
select
i.subscription_id
, i.id as invoice_id
from stripe._invoices as i
inner join (select subscription_id, max(date) as latest_date from stripe._invoices group by 1) recent
on i.subscription_id = recent.subscription_id
where
i.date = recent.latest_date
),
last_charge as (
select
c.invoice as invoice_id
, c.id as charge_id
, c.captured
from stripe._charges as c
inner join (select invoice, max(created) as latest_charge from stripe._charges group by 1) recent_charges
on c.invoice = recent_charges.invoice
where c.created = recent_charges.latest_charge
),
last_invoice_and_charge as(
select
last_invoice.subscription_id
, last_invoice.invoice_id
, last_charge.charge_id
, last_charge.captured
from last_invoice
left join last_charge
on last_invoice.invoice_id = last_charge.invoice_id
)
select
e.created_at
, e.customer_id
, e.id
, e.subscription_id
, e.plan_id
, e.plan_interval
, e.mrr_type
, e.subscription_mrr_value
, e.type
, c.invoice_id
, c.charge_id
, c.captured
from revenue_events as e
left join last_invoice_and_charge as c
on e.subscription_id = c.subscription_id
where e.mrr_type = 'churn'
and e.created_at > (current_date - 730)
Great, we now have around 80 thousand churn events from the past two years in a dataframe. The dataset includes the last invoice ID for that subscription, the last charge ID for that invoice, as well as whether or not the last charge for that invoice was captured. If the last charge for that subscription was not captured, we can assume that it failed, and that this churn event is an involuntary one.
Data tidying
In order to answer the first question regarding the ratio of involuntary churn to total churn, we’ll want to group the churn events by a certain time-frame. In this analysis I’ll choose to group by month, but we could also do so by week. Let’s extrace the month from the created_at
column.
# Extract month
events <- events %>%
mutate(month = as.Date(paste0(format(created_at, "%Y-%m"), "-01")))
There are values with NAs in this dataset. If we take a closer look at these subscriptions and invoices, we’ll notice that the subscriptions do not have any charges associated with them in the stripe._charges
table! That seems odd - something to look into in the future.
For now, let’s group the churn data by month and calculate some summary statistics. We’ll calculate the number of churn events, the number of customers lost, and the MRR amount lost to churn.
# Group churn events by month
by_month <- events %>%
group_by(month, plan_interval, captured) %>%
summarise(events = n_distinct(id),
customers = n_distinct(customer_id),
mrr = sum(subscription_mrr_value)) %>%
mutate(percent_of_events = events / sum(events),
percent_of_mrr = mrr / sum(mrr)) %>%
filter(month != '2015-07-01' & month != '2017-07-01')
We’re now ready for some exploratory analysis.
Exploratory analysis
What percentage of all churn events is made up of involuntary churn?
Overall it looks like there is a clear, decreasing trend. Torwards the end of 2015, around 40% of churn events ended with a failed charge! That has decreased to around 30% now, which is still a high percentage. Let’s take a closer look at June 2017.
## # A tibble: 3 x 7
## # Groups: month [1]
## month captured events customers mrr percent_of_events
## <date> <lgl> <int> <int> <dbl> <dbl>
## 1 2017-06-01 FALSE 1050 1036 19382.833 0.29812606
## 2 2017-06-01 TRUE 2326 2315 38838.333 0.66042022
## 3 2017-06-01 NA 146 21 4368.667 0.04145372
## # ... with 1 more variables: percent_of_mrr <dbl>
In June, there were 3522 churn events that accounted for $62K in lost MRR. Of those 3522 churn events, Around 30% came from subscriptions whose latest invoice was not paid.
Let’s dig in a little deeper into this data, and calculate the percent of total churn that is involuntary for subscriptions that are billed monthly and subscriptions that are billed annually.
This graph makes it clear that involuntary churn is much higher for subscriptions that are billed annually. This makes sense intuitively, as there is a higher cost and a much longer time between payments. In February 2016, over 60% of churned subscriptions for annual plans ended with a failed payment!
How much MRR does this represent?
The MRR amount lost to involuntary churn is quite high. Around 15 to 20K is lost each month! This value is increasing, even though the percent of total churn that is involuntary is decreasing. This can occur when the value of each churn event increases, and the overall number of churn events also increases.
Payment information
Payments fail for different reasons, some of which we can control. We may be able to assume that charges are less likely to fail if we have more information about the customer, e.g. his or her address.
Let’s check this assumption by looking at our charges in the past, and see how many of them had infomation like addresses and zip codes. We can get this data by querying the tables in Stripe’s Sigma tool. We’ll use the following query to gather the number of charges, successful or failed, each month, as well as whether or not the cards had addresses and zip codes. We’ll also be able to segment by the card type.
with charge_information as (
select
date_trunc('month', charges.created) as month,
charges.card_brand,
charges.id as charge_id,
charges.captured,
charges.card_country,
trim(charges.card_address_zip) as card_address_zip,
trim(charges.card_address_line1) as card_address_line1
from charges
left join balance_transactions
on charges.id = balance_transactions.source_id
)
select
date_format(month, '%Y-%m') as month,
coalesce(card_brand, 'Non-card or Other') as card,
captured,
card_country,
length(card_address_zip) > 0 as has_zip,
length(card_address_line1) > 0 as has_address,
count(distinct charge_id) as charges
from charge_information
group by 1, 2, 3, 4, 5, 6
order by 1 desc
To begin, let’s start by just visualizing the percentage of charges that were captured over time.
We can see that where was a decreasing trend from Buffer’s inception to the middle of 2014 - a decreasing percentage of charges were successfully paid. However, beginning around March 2016, the percentage of charges captured has been trending upward. We started collecting customers’ zip codes around March 2016, which may suggest that collecting zip codes decreased the rate of involuntary churn.
Let’s segment this data by whether or not we had the customer’s zip code at the time of the charge. We’ll filter to only graph data after March of 2016
Wow, we can see clearly that charges in which we have users’ zip codes fail less often than charges in which we don’t. We can also segment the data by whether or not we collected the customers’ addresses, however it doesn’t quite seem like we’ve collected customers’ addresses.
Let’s see if the type of credit card makes any difference.
Certain types of credit cards clearly outperform others in terms of charges captured. AMEX cards have a significantly higher capture rate than Visa or Mastercard. Discover, Diners Club, and JBC all don’t seem to have large enough sample sizes to tell for sure.
Let’s plot the capture rate for each of these cards, and segment each by whether or not we had a zip code.
For each type of card, having the zip code improves the capture rate. Awesome! These graphs suggest that having more information decreases the likelihood of a failed payment.
Therefore I might suggest an experiment in which we also ask for users’ addresses, if they are in the US.
How does the country affect the success rate?
Let’s first plot out the success rate for US and non-US cards.
# determine if the card is from the US
payments <- payments %>% mutate(is_us = (card_country == 'US'))
Now let’s make the plot.
Now let’s plot out the success rate for each country that has over 3000 charges.
Israel, Germany, Argentina, Hong Kong, The Netherlands, Singapore, New Zealand, The United States, and Great Britain have the highest success rates, while Saudi Arabia, Turkey, Mexico, India, UAE, Brazil, and South Africa have the lowest success rates.
What effect to the reminder emails have?
This is harder to measure, but we can still do a bit of exploration with our dunning emails. We first need to gather the data, which we can do by querying the user_email_actions
table.
select
created_at
, email
, event_type
, user_id
, count(*)
from user_email_actions
where email like '%dunning%'
and created_at >= (current_date - 365)
group by 1, 2, 3, 4
We can quickly see how many of each email we’ve sent in the past year.
table(email_actions$email)
##
## dunning-phase-1 dunning-phase-2
## 56507 28313
## dunning-phase-3 emails_dunning_phase_1_email
## 24612 4331
## emails_dunning_phase_2_email emails_dunning_phase_3_email
## 3015 1907
## emails_final_dunning_email
## 248
Because it seems that there are different names for the same email, I’m going to rename them.
# Replace emails_dunning_x names
email_actions$email <- gsub("emails_dunning_phase_1_email", "dunning-phase-1", email_actions$email)
email_actions$email <- gsub("emails_dunning_phase_2_email", "dunning-phase-2", email_actions$email)
email_actions$email <- gsub("emails_dunning_phase_3_email", "dunning-phase-3", email_actions$email)
email_actions$email <- gsub("emails_final_dunning_email", "dunning-final-email", email_actions$email)
Now let’s count the emails again.
table(email_actions$email)
##
## dunning-final-email dunning-phase-1 dunning-phase-2
## 248 60838 31328
## dunning-phase-3
## 26519
It’s good to see the numbers decrease. This suggests that users took action by either updating their payment method or canceling their subscription. Now let’s look at the email actions for each email. To do this, let’s group by month and count the number of users that took each type of action.
# Group by month
actions_by_month <- email_actions %>%
group_by(month, email, event_type) %>%
summarise(users = n_distinct(user_id))
Now we need to spread the data, so that each month, email, and event_type combination has a user count in it’s own column.
library(tidyr)
## Warning: package 'tidyr' was built under R version 3.4.1
# Spread data
wide_actions <- actions_by_month %>%
spread(event_type, users)
For some reason, we only have the proper email counts from May 2017 onwards! Let’s see if we can calculate open rates, after filtering out incomplete months.
# Filter out incomplete months and calculate open rate
wide_actions <- wide_actions %>%
filter(month >= '2017-05-01') %>%
mutate(open_rate = email_opens / emails,
ctr = email_clicks / emails)
Now let’s plot the open rates for each month.
What if we instead wanted to look at click through rates? We can define this as the proportion of users that were sent the email that clicked through the CTA.
These seem like fairly decent open rates, but low click-thru rates. Unfortunately, we don’t track a client_id
for these events, so I’m not sure how we can see how many of these emails were opened on a mobile device. We might need to use SendGrid or Mailchimp for that.
Conclusions
The data suggests that involuntary churn makes up a significant portion of our overall churn, over 30%. This ratio has been decreasing substantially over the past two years. Subscriptions that are billed annually have much higher involuntary churn rates than those charged monthly.
Involuntary churn accounts for around 15-20 thousand dollars in MRR lost each month.
Having the customer’s zip code decreases the rate in which payments fail, and I would suggest experimenting with collecting users’ addresses, to further decrease the failed payment rate.
Our dunning emails have relatively high open rates and decent click through rates, but we are unable to tell at the moment how many are opened on mobile devices. We are also unable to say, or estimate, the effects that the emails have on involuntary churn at this time. We may need to run a controlled experiment to make that estimate. :)
In the future, I would like to calculate involuntary churn rates segmented by country. A charge may be more likely to fail if the customer does not live in the United States. We may be able to mitigate some of this risk by asking for more information about the customer when we try to collect payments.