This project will assure you have mastered the subjects covered in the statistics lessons. The hope is to have this project be as comprehensive of these topics as possible. Good luck!
A/B tests are very commonly performed by data analysts and data scientists. It is important that you get some practice working with the difficulties of these.
For this project, you will be working to understand the results of an A/B test run by an e-commerce website. Your goal is to work through this notebook to help the company understand if they should implement the new page, keep the old page, or perhaps run the experiment longer to make their decision.
To get started, let's import our libraries.
import pandas as pd
import numpy as np
import random
import matplotlib.pyplot as plt
%matplotlib inline
#We are setting the seed to assure you get the same answers on quizzes as we set up
random.seed(42)
1.
Now, read in the ab_data.csv
data. Store it in df
.
a. Read in the dataset and take a look at the top few rows here:
# Read in the dataset
df = pd.read_csv('ab_data.csv')
# Return the top 5 rows
df.head()
user_id | timestamp | group | landing_page | converted | |
---|---|---|---|---|---|
0 | 851104 | 2017-01-21 22:11:48.556739 | control | old_page | 0 |
1 | 804228 | 2017-01-12 08:01:45.159739 | control | old_page | 0 |
2 | 661590 | 2017-01-11 16:55:06.154213 | treatment | new_page | 0 |
3 | 853541 | 2017-01-08 18:28:03.143765 | treatment | new_page | 0 |
4 | 864975 | 2017-01-21 01:52:26.210827 | control | old_page | 1 |
b. Use the cell below to find the number of rows in the dataset.
# The first parameter returned by shape gives the number of rows
df.shape[0]
294478
c. The number of unique users in the dataset.
df['user_id'].nunique()
290584
d. The proportion of users converted.
df['converted'].mean()
0.11965919355605512
e. The number of times the new_page
and treatment
don't match.
# There are two cases where 'new_page' and 'treatment' won't match:
# (1) A user is in the 'treatment' group, but they are not presented with the 'new page'
# (2) A user is in the 'control' group, but they are presented with the 'new page'
df.query('(group == "treatment" & landing_page != "new_page") | (group == "control" & landing_page == "new_page")').shape[0]
3893
f. Do any of the rows have missing values?
df.isnull().sum()
user_id 0 timestamp 0 group 0 landing_page 0 converted 0 dtype: int64
As we can see above, none of our columns contain null values.
2.
For the rows where treatment does not match with new_page or control does not match with old_page, we cannot be sure if this row truly received the new or old page.
Store your new dataframe in df2.
# Create a new dataframe that drops all rows that meet the criteria in the query in step 1(e) above
df2 = df.drop(df.query('(group == "treatment" & landing_page != "new_page") | (group == "control" & landing_page == "new_page")').index)
# Double Check all of the correct rows were removed - this should be 0
df2[((df2['group'] == 'treatment') == (df2['landing_page'] == 'new_page')) == False].shape[0]
0
3.
Use df2 and the cells below to answer questions.
a. How many unique user_ids are in df2?
df2['user_id'].nunique()
290584
b. There is one user_id repeated in df2. What is it?
#First, confirm there is only 1 duplicated user, then print the id
print('The number of duplicated user ids in df2 is {}'.format(sum(df2['user_id'].duplicated())))
print('This user id is {}'.format(df2[df2.duplicated(['user_id'], keep='last')]['user_id'].values[0]))
The number of duplicated user ids in df2 is 1 This user id is 773192
c. What is the row information for the repeat user_id?
df2[df2.duplicated(['user_id'], keep=False)]
user_id | timestamp | group | landing_page | converted | |
---|---|---|---|---|---|
1899 | 773192 | 2017-01-09 05:37:58.781806 | treatment | new_page | 0 |
2893 | 773192 | 2017-01-14 02:55:59.590927 | treatment | new_page | 0 |
d. Remove one of the rows with a duplicate user_id, but keep your dataframe as df2.
# Drop the oldest row (i.e. the one with the oldest timestamp)
df2 = df2.drop(df2[df2['timestamp'] == '2017-01-09 05:37:58.781806'].index)
# Make sure the duplicate has been deleted. This should return 0.
sum(df2['user_id'].duplicated())
0
4.
Use df2 in the cells below to answer the following questions.
a. What is the probability of an individual converting regardless of the page they receive?
df2.converted.mean()
0.11959708724499628
b. Given that an individual was in the control
group, what is the probability they converted?
df2[df2['group'] == 'control']['converted'].mean()
0.1203863045004612
c. Given that an individual was in the treatment
group, what is the probability they converted?
df2[df2['group'] == 'treatment']['converted'].mean()
0.11880806551510564
d. What is the probability that an individual received the new page?
# Divide the number of times the new page was served by the total number of times a landing page was served
df2[df2['landing_page'] == 'new_page']['landing_page'].count()/df2['landing_page'].count()
0.50006194422266881
# What is the time interval of our experiment (i.e. was it run long enough)
df2['timestamp'].min(), df2['timestamp'].max()
('2017-01-02 13:42:05.378582', '2017-01-24 13:41:54.460509')
We can see from the above that our experiment has run roughly 22 days.
e. Consider your results from parts (a) through (d) above, and explain below whether you think there is sufficient evidence to conclude that the new treatment page leads to more conversions.
Answer:
The probability that a user converts on the old page is only 0.15% higher than the probability that a user converts on the new page, i.e. the difference is a very slim margin. Additionally, the test has only be running for 22 days. It is possible, for example, that the test hasn't been run long enough to account for change aversion. There does not appear to be sufficient evidence yet to make a conclusion.
Notice that because of the time stamp associated with each event, you could technically run a hypothesis test continuously as each observation was observed.
However, then the hard question is do you stop as soon as one page is considered significantly better than another or does it need to happen consistently for a certain amount of time? How long do you run to render a decision that neither page is better than another?
These questions are the difficult parts associated with A/B tests in general.
1.
For now, consider you need to make the decision just based on all the data provided. If you want to assume that the old page is better unless the new page proves to be definitely better at a Type I error rate of 5%, what should your null and alternative hypotheses be? You can state your hypothesis in terms of words or in terms of $p_{old}$ and $p_{new}$, which are the converted rates for the old and new pages.
Answer:
$H_0$: $p_{new}$ ≤ $p_{old}$
$H_1$: $p_{new}$ > $p_{old}$
or
$H_0$: $p_{new}$ - $p_{old}$ ≤ 0
$H_1$: $p_{new}$ - $p_{old}$ > 0
2.
Assume under the null hypothesis, $p_{new}$ and $p_{old}$ both have "true" success rates equal to the converted success rate regardless of page - that is $p_{new}$ and $p_{old}$ are equal. Furthermore, assume they are equal to the converted rate in ab_data.csv regardless of the page.
Use a sample size for each page equal to the ones in ab_data.csv.
Perform the sampling distribution for the difference in converted between the two pages over 10,000 iterations of calculating an estimate from the null.
a. What is the conversion rate for $p_{new}$ under the null?
# As per above, we assume p_new is the converted rate regardless of the page.
p_new = df2['converted'].mean()
p_new
0.11959708724499628
b. What is the conversion rate for $p_{old}$ under the null?
# Again, we assume p_old is the converted rate regardless of the page.
# This should be equal to p_new
p_old = df2['converted'].mean()
p_old
0.11959708724499628
c. What is $n_{new}$, the number of individuals in the treatment group?
n_new = df2[df2['group'] == 'treatment']['user_id'].count()
n_new
145310
d. What is $n_{old}$, the number of individuals in the control group?
n_old = df2[df2['group'] == 'control']['user_id'].count()
n_old
145274
e. Simulate $n_{new}$ transactions with a conversion rate of $p_{new}$ under the null. Store these $n_{new}$ 1's and 0's in new_page_converted.
new_page_converted = np.random.choice([0,1], size = n_new, p = [1 - p_new, p_new])
f. Simulate $n_{old}$ transactions with a conversion rate of $p_{old}$ under the null. Store these $n_{old}$ 1's and 0's in old_page_converted.
old_page_converted = np.random.choice([0,1], size = n_old, p = [1 - p_old, p_old])
g. Find $p_{new}$ - $p_{old}$ for your simulated values from part (e) and (f).
# Subtract the mean of the simulated new page conversions from the mean of the simulated old page conversions
new_page_converted.mean() - old_page_converted.mean()
0.0011818303079598469
h. Create 10,000 $p_{new}$ - $p_{old}$ values using the same simulation process you used in parts (a) through (g) above. Store all 10,000 values in a NumPy array called p_diffs.
# Initialize p_diffs
p_diffs = []
# Simulate and create 10,000 p_diff values
for _ in range(10000):
new_page_converted = np.random.choice([0,1], size = n_new, p = [1 - p_new, p_new])
old_page_converted = np.random.choice([0,1], size = n_old, p = [1 - p_old, p_old])
p_diffs.append(new_page_converted.mean() - old_page_converted.mean())
i. Plot a histogram of the p_diffs. Does this plot look like what you expected? Use the matching problem in the classroom to assure you fully understand what was computed here.
# First, convert p_diffs into a numpy array
p_diffs = np.array(p_diffs)
# Plot the histogram of p_diffs
plt.hist(p_diffs);
The plot resembles a normal distribution, which is what I expected given the Central Limit Theorem: "With a large enough sample size, the sampling distribution of the mean will be normally distributed."
j. What proportion of the p_diffs are greater than the actual difference observed in ab_data.csv?
# First calculate the actual difference observed in ab_data.csv
actual_diff = df2[df2['group'] == 'treatment']['converted'].mean() - df2[df2['group'] == 'control']['converted'].mean()
actual_diff
-0.0015782389853555567
# Calculate the proportion of those p_diffs that are greater than the actual difference
(p_diffs > actual_diff).mean()
0.90610000000000002
k. Please explain what you just computed in part j. What is this value called in scientific studies? What does this value mean in terms of whether or not there is a difference between the new and old pages?
Answer:
We have just computed the p-value in part j above. If the p-value is less than the type I error threshold, then we have evidence to reject the null hypothesis and choose the alternative. In this case, however, our p-value is substantially higher than the threshold, therefore we have failed to reject the null hypothesis. In practical terms, this means that there is not enough evidence that the new page outperforms the old page.
l. We could also use a built-in to achieve similar results. Though using the built-in might be easier to code, the above portions are a walkthrough of the ideas that are critical to correctly thinking about statistical significance. Fill in the below to calculate the number of conversions for each page, as well as the number of individuals who received each page. Let n_old
and n_new
refer the the number of rows associated with the old page and new pages, respectively.
import statsmodels.api as sm
convert_old = df2[df2['group'] == 'control']['converted'].sum()
convert_new = df2[df2['group'] == 'treatment']['converted'].sum()
n_old = df2[df2['group'] == 'control']['user_id'].count()
n_new = df2[df2['group'] == 'treatment']['user_id'].count()
/opt/conda/lib/python3.6/site-packages/statsmodels/compat/pandas.py:56: FutureWarning: The pandas.core.datetools module is deprecated and will be removed in a future version. Please use the pandas.tseries module instead. from pandas.core import datetools
print('# conversions on old page: {}'.format(convert_old))
print('# conversions on new page: {}'.format(convert_new))
print('# rows associated with the old page: {}'.format(n_old))
print('# rows associated with the new page: {}'.format(n_new))
# conversions on old page: 17489 # conversions on new page: 17264 # rows associated with the old page: 145274 # rows associated with the new page: 145310
m. Now use stats.proportions_ztest
to compute your test statistic and p-value. Here is a helpful link on using the built in.
z_score, p_value = sm.stats.proportions_ztest([convert_new, convert_old], [n_new, n_old], alternative='larger')
print('z-score: {}'.format(z_score))
print('p-value: {}'.format(p_value))
z-score: -1.3109241984234394 p-value: 0.9050583127590245
n. What do the z-score and p-value you computed in the previous question mean for the conversion rates of the old and new pages? Do they agree with the findings in parts j. and k.?
Answer:
The p-value is very close (almost the same) to that which we calculated in part j above, therefore we come to the same conculsion about not rejecting the null hypothesis.
Similar to the p-value, we can also use the z-score to determine whether or not to reject the null hypothesis. According to Wikipedia, the z-score is "the number of standard deviations by which the value of a raw score (i.e., an observed value or data point) is above or below the mean value of what is being observed or measured." Given our type I error threshold of 5%, we assume a confidence interval of 95%. If we consult a z-score table, we see that z-score values that fall within this interval are between -1.96 (the lower 2.5% threshold) and 1.96 (the highest 2.5% threshold). Our z-score of -1.31 falls within our confidence interval, therefore we do not reject the null hypothesis. This corresponds to our result in part k above.
1.
In this final part, you will see that the result you achieved in the A/B test in Part II above can also be achieved by performing regression.
a. Since each row is either a conversion or no conversion, what type of regression should you be performing in this case?
Answer:
Logistic Regression
b. The goal is to use statsmodels to fit the regression model you specified in part a. to see if there is a significant difference in conversion based on which page a customer receives. However, you first need to create in df2 a column for the intercept, and create a dummy variable column for which page each user received. Add an intercept column, as well as an ab_page column, which is 1 when an individual receives the treatment and 0 if control.
# Add the intercept column
df2['intercept'] = 1
# Get the dummy columns for the group
df2[['control', 'ab_page']] = pd.get_dummies(df2['group'])
# Drop the unnecessary column for 'control'
df2.drop('control', axis=1, inplace=True)
# Look at the first few rows of our resulting dataframe
df2.head()
user_id | timestamp | group | landing_page | converted | intercept | ab_page | |
---|---|---|---|---|---|---|---|
0 | 851104 | 2017-01-21 22:11:48.556739 | control | old_page | 0 | 1 | 0 |
1 | 804228 | 2017-01-12 08:01:45.159739 | control | old_page | 0 | 1 | 0 |
2 | 661590 | 2017-01-11 16:55:06.154213 | treatment | new_page | 0 | 1 | 1 |
3 | 853541 | 2017-01-08 18:28:03.143765 | treatment | new_page | 0 | 1 | 1 |
4 | 864975 | 2017-01-21 01:52:26.210827 | control | old_page | 1 | 1 | 0 |
c. Use statsmodels to instantiate your regression model on the two columns you created in part b., then fit the model using the two columns you created in part b. to predict whether or not an individual converts.
# Instantiate the regression model
logit_mod = sm.Logit(df2['converted'], df2[['intercept', 'ab_page']])
# Fit the model
results = logit_mod.fit()
Optimization terminated successfully. Current function value: 0.366118 Iterations 6
d. Provide the summary of your model below, and use it as necessary to answer the following questions.
results.summary2()
Model: | Logit | No. Iterations: | 6.0000 |
Dependent Variable: | converted | Pseudo R-squared: | 0.000 |
Date: | 2021-05-20 17:53 | AIC: | 212780.3502 |
No. Observations: | 290584 | BIC: | 212801.5095 |
Df Model: | 1 | Log-Likelihood: | -1.0639e+05 |
Df Residuals: | 290582 | LL-Null: | -1.0639e+05 |
Converged: | 1.0000 | Scale: | 1.0000 |
Coef. | Std.Err. | z | P>|z| | [0.025 | 0.975] | |
---|---|---|---|---|---|---|
intercept | -1.9888 | 0.0081 | -246.6690 | 0.0000 | -2.0046 | -1.9730 |
ab_page | -0.0150 | 0.0114 | -1.3109 | 0.1899 | -0.0374 | 0.0074 |
e. What is the p-value associated with ab_page? Why does it differ from the value you found in Part II?
Answer:
The p-value associated with ab_page is 0.1899.
This differs from the value found in Part II because in this case our null and alternative hypotheses are slightly different:
$H_0$: $p_{new}$ - $p_{old}$ = 0
$H_1$: $p_{new}$ - $p_{old}$ ≠ 0
Note that our test in Part II was one-sided, whereas here in Part III we are using a two-sided test. As a result, we would expect different p-values.
However, given that our p-value is still greater than the type I error threshold (or alpha), we still fail to reject the hypothesis.
f. Now, you are considering other things that might influence whether or not an individual converts. Discuss why it is a good idea to consider other factors to add into your regression model. Are there any disadvantages to adding additional terms into your regression model?
Answer:
It is a good idea to consider adding other factors into our model, especially if we want to improve our predictions or determine how other factors might influence our results. For example, in our case we might want to take into account whether or not we have a returning customer, the geographic location of a customer, age and/or other demographic characteristics.
A disadvantage of adding additional terms is that it complicates your model. Also, if one or more of the terms are correlated, then this could cause errors in our model (for example, coefficents being flipped from what we would expect.)
g. Now along with testing if the conversion rate changes for different pages, also add an effect based on which country a user lives in. You will need to read in the countries.csv dataset and merge together your datasets on the appropriate rows. Here are the docs for joining tables.
Does it appear that country had an impact on conversion? Don't forget to create dummy variables for these country columns.
# Read in the new dataframe
df_countries = pd.read_csv('countries.csv')
# Print the first few rows
df_countries.head()
user_id | country | |
---|---|---|
0 | 834778 | UK |
1 | 928468 | US |
2 | 822059 | UK |
3 | 711597 | UK |
4 | 710616 | UK |
# Join the two datasets and create a new dataframe
df3 = df2.join(df_countries.set_index('user_id'), on='user_id')
# Print the first few rows
df3.head()
user_id | timestamp | group | landing_page | converted | intercept | ab_page | country | |
---|---|---|---|---|---|---|---|---|
0 | 851104 | 2017-01-21 22:11:48.556739 | control | old_page | 0 | 1 | 0 | US |
1 | 804228 | 2017-01-12 08:01:45.159739 | control | old_page | 0 | 1 | 0 | US |
2 | 661590 | 2017-01-11 16:55:06.154213 | treatment | new_page | 0 | 1 | 1 | US |
3 | 853541 | 2017-01-08 18:28:03.143765 | treatment | new_page | 0 | 1 | 1 | US |
4 | 864975 | 2017-01-21 01:52:26.210827 | control | old_page | 1 | 1 | 0 | US |
# Determine the unique values for country in order to get the dummy values
df3['country'].unique()
array(['US', 'CA', 'UK'], dtype=object)
# Add dummy columns
df3[['CA', 'UK', 'US']] = pd.get_dummies(df3['country'])
# Drop unnecessary column
df3.drop('CA', axis=1, inplace=True)
# Print the first few rows
df3.head()
user_id | timestamp | group | landing_page | converted | intercept | ab_page | country | UK | US | |
---|---|---|---|---|---|---|---|---|---|---|
0 | 851104 | 2017-01-21 22:11:48.556739 | control | old_page | 0 | 1 | 0 | US | 0 | 1 |
1 | 804228 | 2017-01-12 08:01:45.159739 | control | old_page | 0 | 1 | 0 | US | 0 | 1 |
2 | 661590 | 2017-01-11 16:55:06.154213 | treatment | new_page | 0 | 1 | 1 | US | 0 | 1 |
3 | 853541 | 2017-01-08 18:28:03.143765 | treatment | new_page | 0 | 1 | 1 | US | 0 | 1 |
4 | 864975 | 2017-01-21 01:52:26.210827 | control | old_page | 1 | 1 | 0 | US | 0 | 1 |
# Instantiate the new regression model
logit_mod = sm.Logit(df3['converted'], df3[['intercept', 'ab_page','UK', 'US']])
# Fit the model
results = logit_mod.fit()
# Print summary of the results
results.summary2()
Optimization terminated successfully. Current function value: 0.366113 Iterations 6
Model: | Logit | No. Iterations: | 6.0000 |
Dependent Variable: | converted | Pseudo R-squared: | 0.000 |
Date: | 2021-05-20 17:55 | AIC: | 212781.1253 |
No. Observations: | 290584 | BIC: | 212823.4439 |
Df Model: | 3 | Log-Likelihood: | -1.0639e+05 |
Df Residuals: | 290580 | LL-Null: | -1.0639e+05 |
Converged: | 1.0000 | Scale: | 1.0000 |
Coef. | Std.Err. | z | P>|z| | [0.025 | 0.975] | |
---|---|---|---|---|---|---|
intercept | -2.0300 | 0.0266 | -76.2488 | 0.0000 | -2.0822 | -1.9778 |
ab_page | -0.0149 | 0.0114 | -1.3069 | 0.1912 | -0.0374 | 0.0075 |
UK | 0.0506 | 0.0284 | 1.7835 | 0.0745 | -0.0050 | 0.1063 |
US | 0.0408 | 0.0269 | 1.5161 | 0.1295 | -0.0119 | 0.0934 |
We can see from the results above that the p-values are above the 0.05 threshold, therefore we fail to reject the null hypothesis. We can conclude that country does not affect conversions.
h. Though you have now looked at the individual factors of country and page on conversion, we would now like to look at an interaction between page and country to see if there significant effects on conversion. Create the necessary additional columns, and fit the new model.
Provide the summary results, and your conclusions based on the results.
# Create interaction variables between page and country
df3['ab_page_UK'] = df3['ab_page']*df3['UK']
df3['ab_page_US'] = df3['ab_page']*df3['US']
# Instantiate the new regression model
logit_mod = sm.Logit(df3['converted'], df3[['intercept', 'ab_page','UK', 'US', 'ab_page_UK', 'ab_page_US']])
# Fit the model
results = logit_mod.fit()
# Print summary of the results
results.summary2()
Optimization terminated successfully. Current function value: 0.366109 Iterations 6
Model: | Logit | No. Iterations: | 6.0000 |
Dependent Variable: | converted | Pseudo R-squared: | 0.000 |
Date: | 2021-05-20 17:55 | AIC: | 212782.6602 |
No. Observations: | 290584 | BIC: | 212846.1381 |
Df Model: | 5 | Log-Likelihood: | -1.0639e+05 |
Df Residuals: | 290578 | LL-Null: | -1.0639e+05 |
Converged: | 1.0000 | Scale: | 1.0000 |
Coef. | Std.Err. | z | P>|z| | [0.025 | 0.975] | |
---|---|---|---|---|---|---|
intercept | -2.0040 | 0.0364 | -55.0077 | 0.0000 | -2.0754 | -1.9326 |
ab_page | -0.0674 | 0.0520 | -1.2967 | 0.1947 | -0.1694 | 0.0345 |
UK | 0.0118 | 0.0398 | 0.2957 | 0.7674 | -0.0663 | 0.0899 |
US | 0.0175 | 0.0377 | 0.4652 | 0.6418 | -0.0563 | 0.0914 |
ab_page_UK | 0.0783 | 0.0568 | 1.3783 | 0.1681 | -0.0330 | 0.1896 |
ab_page_US | 0.0469 | 0.0538 | 0.8718 | 0.3833 | -0.0585 | 0.1523 |
Once again, our p-values are still above the 0.05 threshold and we fail to reject the null hypothesis. The interactions do not appear to impact conversions.
We used three different methods to analyze whether or not to launch a new landing page:
Using probability we did not find that our new landing page resulted in more conversions. During A/B testing and logically regression, we used the p-value to assess whether are results were statistically significant enough to conclude that the new landing page performed better than the old page. In each case, we were unable to reject our null hypothesis: i.e. that the old page was as effective or moreso in resulting in a conversion.
Given these results, we cannot recommend changing the landing page to the new page.