<jupyter_start><jupyter_text>Modelling Afterpay's customer growth> How can we model the growth in Afterpay's customer base?- toc: true - badges: true- comments: true- categories: [Bayesian,Finance]    - image: images/20-08-13-Modelling-Afterpays-Customer-Growth/header.png  The ContextFounded in 2014, [Afterpay](https://www.afterpay.com/index) is a wildly successful Australian fintech startup. With a market capitalisation of circa 14 billion USD, it has rapidly grown to be a one of Australia's largest companies. In summary, Afterpay allows customers to pay for products in 4 separate payments, and retailers stores a margin for this. I'm interested in applying Bayesian analysis to understand more about Afterpay, based on the information that it has provided to the market, plus a little common sense.> Important:  Obviously I need a disclaimer. If you use anything I say as the basis for any decision, financial or otherwise, you are an idiot.Now, with that out of the way, let's get started. The ModelFirst off, let's load in a bunch of libraries.<jupyter_code>%matplotlib inline
import pymc3 as pm
import matplotlib.pyplot as plt
import numpy as np

from io import StringIO
import pandas as pd

import math

plt.rcParams["figure.figsize"] = (10,10)
from warnings import filterwarnings
filterwarnings('ignore')<jupyter_output><empty_output><jupyter_text>While reading through Afterpay's releases to the markets, I came across this chart, which appears on page 3 of [this](https://www.afterpaytouch.com/images/APT_ASX-Announcement_Q2-FY18_16-Jan-Final-2.pdf) release.![Image of Afterpay custmer growth](../images/20-08-13-Modelling-Afterpays-Customer-Growth/CustomerGrowth.png)<jupyter_code>csv_data = StringIO('''millions_customers,date,month_count
            0.0,30-sep-2015,0 
            0.0,31-dec-2015,3 
            0.0,31-mar-2016,6 
            0.1,30-jun-2016,9 
            0.2,30-sep-2016,12
            0.4,31-dec-2016,15
            0.6,31-mar-2017,18
            0.8,30-jun-2017,21
            1.1,30-sep-2017,24
            1.5,31-dec-2017,27''')


df = pd.read_csv(csv_data, sep=",")
plt.plot(df.month_count,df.millions_customers,color='r')
plt.ylabel('Millions of customers')
plt.xlabel('Months after launch')
plt.grid()
plt.show()<jupyter_output><empty_output><jupyter_text>From this, we can see a rapid, accelerating growth in the number of customers over time. One model we could use is the [sigmoidal model](https://www.sciencedirect.com/science/article/pii/S1877705811001597/pdf?md5=a6adf115754239aeb12922ec06a20a9e&pid=1-s2.0-S1877705811001597-main.pdf). Also known as "The *S* shaped curve", it's a model where growth starts off slow, accelerates, before slowing again. It's often used in technology adoption, or the introduction of a new product.$$\frac{1}{1+e^{-x}}$$<jupyter_code>x = np.arange(-10,10,0.01)
y = 1/(1+math.e**-x)
plt.plot(x,y,color='k')
plt.grid()
plt.show()<jupyter_output><empty_output><jupyter_text>The model can be modified, to modify the scale (*L*), how fast it grows (*k*), or when the fastest growth occurs (*x0*).$$\frac{L}{1+e^{-k(x-x0)}}$$We can fit this model, to the data provided by Afterpay using PyMC3.We can also utilise priors, to inject things we might know or suspect. For example, while I am open minded about what proportion of Australians may choose to become customers of Afterpay, I limit the model to a maximum of 25M customers (population of Australia).In general, this isn't great practice, because I explicitly exclude the possibility that there could be more than 25M customers, and no amount of data will be able to change my mind.To quote Cromwell : *I beseech you, in the bowels of Christ, think it possible that you may be mistaken.*Let's compose this as a Bayesian Regression problem.<jupyter_code>with pm.Model() as model:
    millions_customers = df.millions_customers.values
    x = df.month_count.values.astype(np.float64)
    L =  pm.Uniform('L', lower = 0, upper = 25) 
    k =  pm.Uniform('k', lower=0, upper=1) 
    x0 = pm.Uniform('x0', lower=0, upper=100) 
    
    customers_predicted = L/(1+math.e**(-k*(x-x0)))
    customers = pm.Normal('customers', mu = customers_predicted, sigma = 0.1, observed = millions_customers)
    
with model:
    trace = pm.sample(draws=10_000,tune=5_000)<jupyter_output>Auto-assigning NUTS sampler...
Initializing NUTS using jitter+adapt_diag...
Multiprocess sampling (2 chains in 2 jobs)
NUTS: [x0, k, L]
Sampling 2 chains, 1 divergences: 100%|██████████| 30000/30000 [01:14<00:00, 401.51draws/s]
There was 1 divergence after tuning. Increase `target_accept` or reparameterize.
The number of effective samples is smaller than 25% for some parameters.<jupyter_text>The ResultsFrom the following plots, we can notice several things. Firstly, and most importantly, our model converged well. Secondly, we can see that according to our model, we expect the number of customers to most likely saturate below 5m people, but it's not impossible that we might have a lot more customers, even 25m.Finally, we can see that there is a lot of uncertainty around the timing of the peak of the growth. This ranges from approximately 2 years to 4 years after the initial introduction of *Afterpay*.<jupyter_code>pm.traceplot(trace);
pm.plot_posterior(trace);
pm.summary(trace)<jupyter_output><empty_output><jupyter_text>Posterior Predictive ChecksLet's see if our model makes sense. By drawing samples from our model, we can further investigate the potential growth rates of Afterpay.<jupyter_code>for i in range(0,1_000):
    x = np.arange(0,30)
    plt.plot(x,trace['L'][i]/(1+math.e**(-trace['k'][i]*(x-trace['x0'][i]))),color='k',alpha=0.01)
plt.plot(df.month_count,df.millions_customers,color='r',label='Reported Customers')

plt.legend()
plt.ylabel('Millions of customers')
plt.xlabel('Months after launch')
plt.show()<jupyter_output><empty_output><jupyter_text>Now let's start forcasting into the future. We can compare what we modelled to the actual customer numbers (3.1M) reported by Afterpay 54 months after they first launched. [Page 6](https://www.afterpaytouch.com/images/27022019-H1-FY20-Results-Presentation.pdf)<jupyter_code>for i in range(0,10_000):
    x = np.arange(0,51)
    plt.plot(x,trace['L'][i]/(1+math.e**(-trace['k'][i]*(x-trace['x0'][i]))),color='k',alpha=0.01)
plt.plot(df.month_count,df.millions_customers,color='r',label='Reported Customers')


plt.scatter(51,3.1,label='Most recent report')
plt.legend()

plt.ylabel('Millions of customers')
plt.xlabel('Months after launch')
plt.show()<jupyter_output><empty_output><jupyter_text>We can also generate a histogram, and compare the predictions of our model with the actual true number of 3.1M.<jupyter_code>x = 51*np.ones(20000)
y = trace['L']/(1+math.e**(-trace['k']*(x-trace['x0'])))                              
plt.hist(y,bins=np.arange(0,25,0.5),alpha=0.5,density=True)
plt.vlines(3.1,0,0.175,label='Most recent report')
        
plt.xlabel('Millions of customers')
plt.legend()
plt.grid()
plt.show()<jupyter_output><empty_output><jupyter_text>ConclusionIn conclusion, Bayesian analysis is a powerful tool, because it allows us to understand how confident we are in our predictions. It's also powerful because we can feed in information that we might already believe. If you are 80% confident that no more than 20% of the Australian population will use Afterpay, then you can feed this in as a prior. While this is a fairly *rough and ready* model It's interesting. We can see clearly that there is a huge amount of uncertainty in the final number of customers we expect Afterpay to have.We also need to keep in mind that we might have picked an inappropriate model, which might not appropriately fit reality.<jupyter_code>import arviz as az
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import pymc3 as pm
import seaborn as sns
import theano.tensor as tt

# Produce normally distributed samples
np.random.seed(1618)
size = 500
mu = 13.
sigma = 5.
samples = np.random.normal(mu, sigma, size)

# Set censoring limits
high = 16.
low = -1.

# Censor samples
censored = samples[(samples > low) & (samples < high)]

# Visualize uncensored and censored data
_, axarr = plt.subplots(ncols=2, figsize=[16, 4], sharex=True, sharey=True)
for i, data in enumerate([samples, censored]):
    sns.distplot(data, ax=axarr[i])
axarr[0].set_title('Uncensored')
axarr[1].set_title('Censored')
plt.show()
# Imputed censored model
n_right_censored = len(samples[samples >= high])
n_left_censored = len(samples[samples <= low])
n_observed = len(samples) - n_right_censored - n_left_censored

with pm.Model() as imputed_censored_model:
    mu = pm.Normal('mu', mu=0., sigma=(high - low) / 2.)
    sigma = pm.HalfNormal('sigma', sigma=(high - low) / 2.)

    right_censored = pm.Bound(pm.Normal, lower=high)(
        'right_censored', mu=mu, sigma=sigma, shape=n_right_censored
    )
    left_censored = pm.Bound(pm.Normal, upper=low)(
        'left_censored', mu=mu, sigma=sigma, shape=n_left_censored
    )

    observed = pm.Normal(
        'observed',
        mu=mu,
        sigma=sigma,
        observed=censored,
        shape=n_observed
    )
pm.model_to_graphviz( imputed_censored_model)
# Imputed censored model
with imputed_censored_model:
    trace = pm.sample()
    pm.plot_posterior(trace, varnames=['mu', 'sigma']);<jupyter_output>Auto-assigning NUTS sampler...
Initializing NUTS using jitter+adapt_diag...
Multiprocess sampling (2 chains in 2 jobs)
NUTS: [left_censored, right_censored, sigma, mu]
Sampling 2 chains, 0 divergences: 100%|██████████| 2000/2000 [00:04<00:00, 451.00draws/s]
{'fraction_non_alphanumeric': 0.11548474435072373, 'fraction_numerical': 0.04102884515255649, 'mean_word_length': 4.68957345971564, 'pattern_counts': {'":': 0, '<': 39, '<?xml version=': 0, '>': 39, 'https://': 4, 'lorem ipsum': 0, 'www.': 4, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '9', 'dir': 'jupyter-structured-clean-dedup', 'id': '603731', 'license': 'Apache-2.0', 'n_tokens_mistral': 3445, 'n_tokens_neox': 3079, 'n_words': 992, 'path': "_notebooks/Drafts/2020-08-14-Modelling-Afterpay's-Customer-Growth-Censored.ipynb", 'repo_name': 'CGCooke/Blog', 'size': '9603'}