Исследование мобильного приложения по продаже продуктов¶
Описание проекта
Заказчик — менеджеры стартапа, продающего продукты питания. Нужно разобраться, как ведут себя пользователи мобильного приложения:
Изучить воронку продаж:
- узнать, как пользователи доходят до покупки
- сколько пользователей доходит до покупки, а сколько — «застревает» на предыдущих шагах?
- на каких именно?
Исследовать результаты A/A/B-эксперимента:
- дизайнеры захотели поменять шрифты во всём приложении, но менеджеры засомневались, что пользователям будет непривычно
- договорились принять решение по результатам A/A/B-теста
- пользователей разбили на 3 группы: 2 контрольные со старыми шрифтами и одну экспериментальную — с новыми
- нужно выяснить, какой шрифт лучше?
Содержание:
- Исследовательский анализ данных
- Анализ воронки событий
- Исследование результатов эксперимента
- Выбор уровня статистической значимости
Описание данных
В таблице logs_exp.csv
логи действий пользователя (событий) мобильного приложения.
EventName
— название события,DeviceIDHash
— уникальный идентификатор пользователя,EventTimestamp
— время события,ExpId
— номер эксперимента: 246 и 247 — контрольные группы, а 248 — экспериментальная.
Общая информация о данных¶
# импорт библиотек
import pandas as pd
import matplotlib.pyplot as plt
from IPython.display import display, Markdown, IFrame
import os
import seaborn as sns
import numpy as np
from datetime import datetime as dt
import matplotlib.ticker as mticker
import scipy.stats as st
from plotly import graph_objects as go
import math as mth
import sys
from statsmodels.stats.proportion import proportions_ztest
# проверка на возможность выполнения Mardown
if 'ipykernel' in sys.modules:
flag_md = 1
else:
flag_md = 0
# чтение файла с данными и сохранение в датафрейм
dir1 = '/datasets/'
dir2 = 'C:/Users/user/Downloads/'
event_pth = 'logs_exp.csv'
if os.path.exists(dir1):
df_event = pd.read_csv(dir1 + event_pth, sep='\t')
elif os.path.exists(dir2):
df_event = pd.read_csv(dir2 + event_pth, sep='\t')
else:
print('Something is wrong')
# датафрейм о событиях `df_event`
# вывод первых 5 строчек
display(df_event.head())
# вывод основной информации
df_event.info()
# гистограмма для столбца `EventName`
sns.countplot(
data=df_event,
x="EventName",
order=df_event['EventName'].value_counts().index
)
plt.title('Распределение событий')
plt.ylabel('Количество')
plt.xlabel('Событие')
plt.xticks(rotation=45)
plt.show()
EventName | DeviceIDHash | EventTimestamp | ExpId | |
---|---|---|---|---|
0 | MainScreenAppear | 4575588528974610257 | 1564029816 | 246 |
1 | MainScreenAppear | 7416695313311560658 | 1564053102 | 246 |
2 | PaymentScreenSuccessful | 3518123091307005509 | 1564054127 | 248 |
3 | CartScreenAppear | 3518123091307005509 | 1564054127 | 248 |
4 | PaymentScreenSuccessful | 6217807653094995999 | 1564055322 | 248 |
<class 'pandas.core.frame.DataFrame'> RangeIndex: 244126 entries, 0 to 244125 Data columns (total 4 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 EventName 244126 non-null object 1 DeviceIDHash 244126 non-null int64 2 EventTimestamp 244126 non-null int64 3 ExpId 244126 non-null int64 dtypes: int64(3), object(1) memory usage: 7.5+ MB
Выводы:
В таблице нет пропущенных значений
Необходимо переименовать столбцы
Необходимо добавить столбцы для даты и времени, даты событий
Таблицу необходимо проверить на наличие дубликатов
Необходимо проверить корректность результатов проведения тестов
Предобработка данных¶
Переименование столбцов¶
# переименование столбцов
df_event = df_event.rename(
columns={
'EventName': 'event_name',
'DeviceIDHash': 'user_id',
'EventTimestamp': 'event_ts',
'ExpId': 'exp_group'
}
)
Добавление столбцов¶
Добавим столбец event_dt
для даты и времени событий и столбец event_date
для даты событий. Преобразуем временную метку в формате Unix из столбца event_ts
.
# создание столбцов
df_event['event_dt'] = pd.to_datetime(df_event['event_ts'], unit='s')
df_event['event_date'] = df_event['event_dt'].dt.date
Проверка наличия дубликатов¶
Проверим наличие явных дубликатов
# вывод названий таблиц и количество явных дубликатов в них
message = (
f'Удалим **{df_event.duplicated().sum()}**'
f' явных дубликатов строк'
) if df_event.duplicated().sum() > 0 else f'Нет явных дубликатов в таблице'
if flag_md == 1:
display(Markdown(message))
else:
print(message.replace("*", ""))
Удалим 413 явных дубликатов строк
# удаление явных дубликатов
df_event = df_event.drop_duplicates()
Проверим наличие неявных дубликатов в столбцах event_name
и exp_group
# вывод отсортированного уникального списка значений в 'event_name'
display(df_event['event_name'].sort_values().unique())
# вывод отсортированного уникального списка значений в 'exp_group'
display(df_event['exp_group'].sort_values().unique())
array(['CartScreenAppear', 'MainScreenAppear', 'OffersScreenAppear', 'PaymentScreenSuccessful', 'Tutorial'], dtype=object)
array([246, 247, 248])
Неявных дубликатов не обнаружено.
Проверка распределения пользователей по тестовым группам¶
Проверим попадание действий одних и тех же user_id
в разные группы тестирования.
# запись количества уникальных групп для каждого user_id в столбец group_count
df_event['group_count'] = df_event.groupby('user_id')['exp_group'].transform('nunique')
# запись в переменную списка user_id, которые находятся в более чем одной группе
multigroup_visitors = df_event.query('group_count > 1')['user_id'].unique()
# вывод результата
message = (
f'Есть **{len(multigroup_visitors)}** посетителей, чьи действия попали в разные группы тестирования.\n\n'
f'Это составляет **{len(multigroup_visitors) / df_event["user_id"].nunique():.0%}** от всех посетителей.\n\n'
f'Исключим из дальнейшего исследования действия пользователей, попавших в разные группы А/А/В теста.'
) if len(multigroup_visitors) > 0 else f'Нет `user_id`, чьи действия попали в разные группы.'
if flag_md == 1:
display(Markdown(message))
else:
print(message.replace("*", ""))
Нет user_id
, чьи действия попали в разные группы.
Выводы:
Переименовали и создали новые столбцы
Удалены явные дубликаты. Неявные дубликаты в таблицах не обнаружены
Система распределения пользователей по тестовым группам отработала корректно
Исследовательский анализ данных¶
Пользователи и события¶
Оценим количество количество событий и пользователей. Рассчитаем среднее количество событий на пользователя.
# количество событий
total_events = len(df_event)
# уникальные пользователи
unique_users = df_event['user_id'].nunique()
# среднее количество событий на пользователя
average_events = total_events / unique_users
# вывод результата
message = (
f'Всего событий: **{total_events:,}**\n\n'
f'Уникальных пользователей: **{unique_users:,}**\n\n'
f'Среднее количество событий на одного пользователя: **{average_events:.2f}**'.replace(",", " ")
)
if flag_md == 1:
display(Markdown(message))
else:
print(message.replace("*", ""))
Всего событий: 243 713
Уникальных пользователей: 7 551
Среднее количество событий на одного пользователя: 32.28
Анализ периода полных данных¶
Найдем максимальную и минимальную даты в логе.
# вывод результата
message = (
f'Минимальная дата: **{df_event["event_date"].min()}**\n\n'
f'Максимальная дата: **{df_event["event_date"].max()}**'
)
if flag_md == 1:
display(Markdown(message))
else:
print(message.replace("*", ""))
Минимальная дата: 2019-07-25
Максимальная дата: 2019-08-07
Оценим графически распределение количества событий в зависимости от даты в разрезе тестовых групп.
# задание размера графика
plt.figure(figsize=(12, 6))
# вывод гистограммы для df_event
sns.histplot(
df_event,
x="event_date",
edgecolor=".3",
linewidth=.5,
hue='exp_group',
multiple='dodge',
shrink=.8
)
plt.title('Распределение событий')
plt.ylabel('Количество событий')
plt.xlabel('Даты')
plt.xticks(ticks=df_event['event_date'].unique()[::1], rotation=45)
plt.show()
Для анализа будем использовать период полных данных с 2019-08-01 по 2019-08-07. Исключим из анализа старые данные и оценим количество "потерянных" событий, пользователей и их доли от исходного количества.
# начальная дата анализа
start_date = pd.to_datetime('2019-08-01')
# исключение старых событий
df_event = df_event.query('event_dt >= @start_date')
# потерянные события
lost_events = total_events - len(df_event)
# доля потерянных событий
rate_lost_events = lost_events / total_events
# потерянные пользователи
lost_users = unique_users - df_event["user_id"].nunique()
# доля потерянных пользователей
rate_lost_users = lost_users / unique_users
# вывод переменных
message = (
f'Исключили: **{lost_events}** событий, **{rate_lost_events:.2%}** от исходного количества\n\n'
f'Исключили: **{lost_users}** пользователей, **{rate_lost_users:.2%}** от исходного количества'
)
if flag_md == 1:
display(Markdown(message))
else:
print(message.replace("*", ""))
Исключили: 2826 событий, 1.16% от исходного количества
Исключили: 17 пользователей, 0.23% от исходного количества
Проверим наличие пользователей из всех трёх тестовых групп.
# распределение пользователей по тестовым группам
df_agg = df_event.groupby('exp_group', as_index=False)['user_id'].nunique()
df_agg.columns = ('exp_group', 'user_count')
df_agg
exp_group | user_count | |
---|---|---|
0 | 246 | 2484 |
1 | 247 | 2513 |
2 | 248 | 2537 |
Выводы
Для анализа будем использовать период полных данных с 2019-08-01 по 2019-08-07.
Старые данные исключили из дальнейшего анализа - потеряли 1.16% событий и 0.23% пользователей
Пользователи присутствуют во всех трех тестовых группах
Анализ воронки событий¶
# количество событий по убыванию
event_counts = df_event['event_name'].value_counts().reset_index()
event_counts
event_name | count | |
---|---|---|
0 | MainScreenAppear | 117328 |
1 | OffersScreenAppear | 46333 |
2 | CartScreenAppear | 42303 |
3 | PaymentScreenSuccessful | 33918 |
4 | Tutorial | 1005 |
# вывод гистограммы для столбца `event_name`
plt.figure(figsize=(12, 6))
sns.histplot(
df_event,
x="event_name",
edgecolor=".3",
linewidth=.5,
shrink=.9
)
plt.title('Распределение событий')
plt.ylabel('Количество')
plt.xlabel('Событие')
plt.show()
Предварительная оценка событий по количеству:
- событие
Tutorial
не входит в последовательную цепочку событий - для подтверждения этой гипотезы оценим распределение пользователей по событиям в числовом и графическом виде.
# распределение пользователей по событиям
df_agg = \
(
df_event.groupby('event_name', as_index=False)['user_id'].nunique()
.sort_values(by='user_id', ascending=False)
)
df_agg.columns = ('event_name', 'user_count')
df_agg
event_name | user_count | |
---|---|---|
1 | MainScreenAppear | 7419 |
2 | OffersScreenAppear | 4593 |
0 | CartScreenAppear | 3734 |
3 | PaymentScreenSuccessful | 3539 |
4 | Tutorial | 840 |
# вывод графика распределения пользователей
plt.figure(figsize=(12, 6))
sns.barplot(
data=df_agg,
x="event_name",
y="user_count",
edgecolor=".3",
linewidth=.5
)
plt.title('Распределение пользователей по событиям')
plt.ylabel('Количество пользователей')
plt.xlabel('Событие')
plt.show()
Выводы:
События выстраиваются в следующий порядок (воронку):
MainScreenAppear
->OffersScreenAppear
->CartScreenAppear
->PaymentScreenSuccessful
Событие
Tutorial
не входит в последовательную цепочку (не является обязательным для совершения покупки) и его можно исключить из дальнейшего расчета воронкиКоличество пользователей ожидаемо убывает на каждом шаге воронки
Расчет воронки событий¶
Исключим событие Tutorial
из дальнейшего расчета воронки.
# исключение события `Tutorial`
df_agg = df_agg.query('event_name != "Tutorial"')
Посчитаем, какая доля пользователей проходит на следующий шаг воронки (от числа пользователей на предыдущем). Запишем значения в столбец conversion
. Последовательность событий воронки сохраним в переменную event_funnel
.
# задание последовательности событий воронки
event_funnel = ['MainScreenAppear', 'OffersScreenAppear', 'CartScreenAppear', 'PaymentScreenSuccessful']
df_agg = pd.DataFrame(df_agg).copy()
df_agg['conversion'] = (
df_agg['user_count'] /
df_agg['user_count'].shift(1)
).fillna(1).apply(lambda x: f'{x:.2%}' if x!=1 else f'{1:.0%}')
df_agg
event_name | user_count | conversion | |
---|---|---|---|
1 | MainScreenAppear | 7419 | 100% |
2 | OffersScreenAppear | 4593 | 61.91% |
0 | CartScreenAppear | 3734 | 81.30% |
3 | PaymentScreenSuccessful | 3539 | 94.78% |
Оценим графически долю пользователей, переходящих на следующий этап воронки. Отобразим на графике значения конверсий из предыдущего события of previous
и из первого события of initial
# вывод диаграммы воронки событий
fig = go.Figure(
go.Funnel(
y = df_agg['event_name'],
x = df_agg['user_count'],
textposition = "inside",
textinfo = "value+percent previous+percent initial",
)
)
fig.update_layout(
width=1000,
height=600,
title='Диаграмма воронки событий',
title_x=0.55
)
fig.write_html('funnel_chart.html')
IFrame('funnel_chart.html', width=1000, height=600)
Вывод:
Больше всего пользователей теряется при переходе с первого на второй шаг - 38% пользователей не достигают события
OffersScreenAppear
От первого события
MainScreenAppear
до оплатыPaymentScreenSuccessful
доходит 48% пользователей
Исследование результатов эксперимента¶
Распределение пользователей по тестовым группам¶
Запишем коды тестовых групп в переменные.
# первая контрольная группа
control_A1 = 246
# вторая контрольная группа
control_A2 = 247
# объединенная контрольная группа
control_AA = 246247
# экспериментальная группа
variant_B = 248
Оценим количественное распределение пользователей по трём тестовым группам.
# распределение пользователей по тестовым группам
df_agg = df_event.groupby('exp_group', as_index=False)['user_id'].nunique()
df_agg.columns = ('exp_group', 'user_group')
# функция для отображения подписей
def label_pct(pct, allusers):
absolute = round(pct / 100. * allusers.sum())
return f'Количество: {absolute},\n доля: {pct:.1f}%'
# вывод круговой диаграммы для`user_count`
plt.figure(figsize=(12, 6))
plt.pie(
df_agg['user_group'],
shadow=True,
autopct=lambda pct: label_pct(pct, df_agg['user_group']),
labels=[
f'{"Экспериментальная" if group == variant_B else "Контрольная"} группа: {group}'
for group in df_agg['exp_group']
]
)
plt.title('Распределение пользователей по тестовым группам')
plt.show()
Вывод
- Пользователи распределены по всем тестовым группам примерно в одинаковом количестве
Проверка корректности разделения на группы (А/А тест)¶
Проверим корректность системы сплитования на контрольных группах. Создадим таблицу с количеством пользователей по группам и событиям.
# создание таблицы по группам/событиям
df_group = \
(
df_event
.groupby(['exp_group', 'event_name'], as_index=False)['user_id'].nunique()
.sort_values(by=['exp_group', 'user_id'], ascending=[True, False])
)
df_group.columns = ('exp_group', 'event_name', 'user_event')
# добавление столбца количества пользователей по группам
df_group = df_group.merge(df_agg, on='exp_group', how='left')
Объединим суммированием пользователей контрольных групп А1 и А2 и добавим их в таблицу по группам и событиям.
# создание объединенной контрольной группы
df_union = \
(
df_group
.query('exp_group == @control_A1 or exp_group == @control_A2')
.groupby('event_name', as_index=False)
.agg(user_event=('user_event', 'sum'),
user_group=('user_group', 'sum'))
.sort_values(by='user_event', ascending=False)
)
df_union.insert(0, 'exp_group', control_AA)
# присоединение объединенной контрольной группы
df_union = pd.concat([df_group, df_union], ignore_index=True)
Создадим функцию для расчета статистической значимости различия между конверсиями в событие двух тестовых групп. Используем двусторонний z-test
для пропорций.
def z_test(successes, trials, alpha, print_result):
# доля успехов в первой группе:
p1 = successes[0][0]/trials[0][0]
# доля успехов во второй группе:
p2 = successes[1][0]/trials[1][0]
# расчет вероятности
p_value = proportions_ztest([successes[0][0],successes[1][0]],[trials[0][0],trials[1][0]])[1]
if print_result==1:
# вывод результатов теста
message = (
f'Результаты теста события: **{successes[0][1]}** для групп: **{trials[0][1]}** '
f'и **{trials[1][1]}**, alpha=**{alpha}**\n\n'
f'В группе **{trials[0][1]}** доля успехов: **{p1:.2%}**\n\n'
f'В группе **{trials[1][1]}** доля успехов: **{p2:.2%}**\n'
)
# проверка гипотезы
message1 = (
f'p_value=**{p_value:.2e}** < {alpha:.2%}\n\n'
f' Отвергаем нулевую гипотезу: между долями есть значимая разница.\n\n'
f'---'
) if p_value < alpha else (
f'p_value=**{p_value:.2%}** >= {alpha:.2%}\n\n'
f'Не удалось отвергнуть нулевую гипотезу: '
f'нет статистически значимых оснований считать доли разными.\n\n'
f'---\n')
if flag_md == 1:
display(Markdown(message))
display(Markdown(message1))
else:
print(message.replace("*", ""))
print(message1.replace("*", ""))
return p_value
Создадим функцию для формирования набора данных для статистического z-test
. Не будем включать в наборы данных событие Tutorial
, как не влияющее на процесс покупки.
def ztest_data(groupA, groupB, alpha, print_result):
results_test = []
# создания списка тестовых групп
exp_group=[groupA, groupB]
# количество пользователей в группе
trials = [[df_union.query('exp_group == @group')['user_group'].iloc[0], group] for group in exp_group]
# количество пользователей, совершивших событие
for current_event in event_funnel:
successes = [[
df_union.query('exp_group == @group & event_name == @current_event')['user_event'].iloc[0],
current_event]
for group in exp_group
]
# вызов функции расчета статистической значимости
p_value = z_test(successes, trials, alpha, print_result)
results_test.append([current_event, p_value])
return results_test
Проверка правильности выбора z-test
¶
Для этого сравним его результаты с результатами t-test
и Mann–Whitney U test
на заведомо одинаковых контрольных группах А1 и А2. Сравним значения p-value
трех статистических методов для каждого события.
Оценим возможность применения t-test
- проверим для группы А1, что выборочное среднее распределено нормально (bootstrap + график qq).
# формирование данных для `t-test` и `Mann–Whitney-test`
df_filtered = df_event.query(
'(exp_group ==@control_A1 or exp_group ==@control_A2) and event_name in @event_funnel'
)
# создание таблицы по группам/событиям/датам
df_group = \
(
df_filtered
.groupby(['exp_group', 'event_name', 'event_date'], as_index=False)['user_id'].nunique()
.sort_values(by=['exp_group', 'event_date'], ascending=[True, True])
)
df_group.columns = ('exp_group', 'event_name', 'event_date', 'user_event')
# распределение пользователей по тестовым группам, событиям и датам
df_agg = df_filtered.groupby(['exp_group', 'event_date'], as_index=False)['user_id'].nunique()
df_agg.columns = ('exp_group', 'event_date', 'user_group')
# добавление столбца количества пользователей по группам и датам
df_group = df_group.merge(df_agg, on=['exp_group', 'event_date'], how='left')
# расчет конверсии
df_group['conversion'] = df_group['user_event'] / df_group['user_group']
# инициализация списка
results = []
# цикл по событиям воронки
for current_event in event_funnel:
# bootstrap среднего значения конверсии
avg_conv = [
df_group.query('exp_group == @control_A1 and event_name == @current_event')['conversion']
.sample(1000, replace=True).mean()
for _ in range(1000)
]
# добавление результатов в список
results.append({
'current_event': current_event,
'avg_conv': avg_conv
})
df_avg = pd.DataFrame(results).explode('avg_conv').reset_index(drop=True)
# гистограммы для выборочного среднего по событиям
plt.figure(figsize=(14, 8))
for i, current_event in enumerate(event_funnel, start=1):
plt.subplot(2, 2, i)
sns.histplot(df_avg.query('current_event == @current_event')['avg_conv'], kde=True)
plt.title(f'Выборочное среднее для {current_event}')
plt.ylabel('Количество') if i % 2 != 0 else plt.ylabel('')
plt.xlabel('Конверсия') if i > 2 else plt.xlabel('')
plt.tight_layout()
plt.show()
# гистограммы для qq-графика по событиям
plt.figure(figsize=(14, 8))
for i, current_event in enumerate(event_funnel, start=1):
plt.subplot(2, 2, i)
st.probplot(df_avg.query('current_event == @current_event')['avg_conv'].astype(float), dist="norm", plot=plt)
plt.title(f'QQ-график для {current_event}')
plt.ylabel('Квантиль наблюдений')
plt.xlabel('Квантиль нормального распределения')
plt.tight_layout()
plt.show()
Вывод:
- Выборочное среднее конверсии в событие в группе А1 распределено нормально
Проведем статистические тесты t-test
и Mann–Whitney U test
и сравним результаты с z-test
# инициализация выходных датафреймов
results_ttest = []
results_mw = []
# цикл по событиям воронки
for current_event in event_funnel:
# формирование результатов `t-test`
p_value = st.ttest_ind(
df_group.query('exp_group ==@control_A1 and event_name ==@current_event')['conversion'],
df_group.query('exp_group ==@control_A2 and event_name ==@current_event')['conversion'], equal_var=False
)[1]
results_ttest.append([current_event, p_value])
# формирование результатов `Mann–Whitney-test`
p_value = st.mannwhitneyu(
df_group.query('exp_group ==@control_A1 and event_name ==@current_event')['conversion'],
df_group.query('exp_group ==@control_A2 and event_name ==@current_event')['conversion'])[1]
results_mw.append([current_event, p_value])
# запись результатов в датафреймы
results_ttest = pd.DataFrame(results_ttest, columns=['event_name', 'ttest_pvalue'])
results_mw = pd.DataFrame(results_mw, columns=['event_name', 'mw_pvalue'])
# результаты `z-test`
results_test = ztest_data(control_A1, control_A2, 0.05, 0)
results_test = pd.DataFrame(results_test, columns=['event_name', 'ztest_pvalue'])
# объединение результатов `z-test` и `t-test` и `Mann–Whitney-test`
results_test = results_test.merge(results_ttest, on='event_name', how='left')
results_test = results_test.merge(results_mw, on='event_name', how='left')
results_test
event_name | ztest_pvalue | ttest_pvalue | mw_pvalue | |
---|---|---|---|---|
0 | MainScreenAppear | 0.757060 | 0.503038 | 0.534965 |
1 | OffersScreenAppear | 0.248095 | 0.205942 | 0.208625 |
2 | CartScreenAppear | 0.228834 | 0.179367 | 0.164918 |
3 | PaymentScreenSuccessful | 0.114567 | 0.128758 | 0.072844 |
Выводы:
На заведомо одинаковых контрольных группах А1 и А2 лучшие показатели, подтверждающие отсутствие различий, показал
z-test
В
t-test
сравнивали среднюю конверсию по дням в А1 и А2. Результат: - выборки одинаковые, хотя и с меньшей вероятностью чем уz-test
В
Mann–Whitney-test
сравнивали среднюю конверсию по дням в А1 и А2. Результаты теста схожие с результатамиt-test
. Но для событияPaymentScreenSuccessful
результат близок к статистически значимому различию между А1 и А2.Для дальнейшего статистического анализа будем использовать
z-test
Посчитаем статистическую значимость отличий между долями посетителей контрольных групп А1/А2 по всем событиям. Сформулируем гипотезы:
H0 - нулевая гипотеза:
- различий между долями в контрольных группах нет
H1 - альтернативная двусторонняя гипотеза
- различия между долями в контрольных группах есть
Проверим гипотезу с помощью Z-теста для пропорций. Если вероятность ошибочно отвергнуть нулевую гипотезу p-value
окажется меньше уровня статистической значимости alpha = 5%
, то отвергнем нулевую гипотезу в пользу альтернативной.
# проверка отличий между А/А
ztest_data(control_A1, control_A2, 0.05, 1);
Результаты теста события: MainScreenAppear для групп: 246 и 247, alpha=0.05
В группе 246 доля успехов: 98.63%
В группе 247 доля успехов: 98.53%
p_value=75.71% >= 5.00%
Не удалось отвергнуть нулевую гипотезу: нет статистически значимых оснований считать доли разными.
Результаты теста события: OffersScreenAppear для групп: 246 и 247, alpha=0.05
В группе 246 доля успехов: 62.08%
В группе 247 доля успехов: 60.49%
p_value=24.81% >= 5.00%
Не удалось отвергнуть нулевую гипотезу: нет статистически значимых оснований считать доли разными.
Результаты теста события: CartScreenAppear для групп: 246 и 247, alpha=0.05
В группе 246 доля успехов: 50.97%
В группе 247 доля успехов: 49.26%
p_value=22.88% >= 5.00%
Не удалось отвергнуть нулевую гипотезу: нет статистически значимых оснований считать доли разными.
Результаты теста события: PaymentScreenSuccessful для групп: 246 и 247, alpha=0.05
В группе 246 доля успехов: 48.31%
В группе 247 доля успехов: 46.08%
p_value=11.46% >= 5.00%
Не удалось отвергнуть нулевую гипотезу: нет статистически значимых оснований считать доли разными.
Выводы:
При заданном уровне статистической значимости
alpha = 5%
разбиение пользователей на группы работает корректноХотя между пользователями контрольных групп есть различия в конверсии, они не являются статистически значимыми
С переходом пользователей на каждый следующий этап воронки происходит увеличение различий между контрольными группами
Проверка результатов экспериментальной группы¶
Сравнение с контрольной группой А1¶
Посчитаем статистическую значимость отличий между долями посетителей контрольной и экспериментальной групп А1/В по всем событиям. Сформулируем гипотезы:
H0 - нулевая гипотеза:
- различий между долями в контрольной и экспериментальной группах нет
H1 - альтернативная двусторонняя гипотеза
- различия между долями в контрольной и экспериментальной группах есть
Проверим гипотезу с помощью Z-теста для пропорций. Если вероятность ошибочно отвергнуть нулевую гипотезу p-value
окажется меньше уровня статистической значимости alpha = 5%
, то отвергнем нулевую гипотезу в пользу альтернативной.
# проверка отличий между А1/В
ztest_data(control_A1, variant_B, 0.05, 1);
Результаты теста события: MainScreenAppear для групп: 246 и 248, alpha=0.05
В группе 246 доля успехов: 98.63%
В группе 248 доля успехов: 98.27%
p_value=29.50% >= 5.00%
Не удалось отвергнуть нулевую гипотезу: нет статистически значимых оснований считать доли разными.
Результаты теста события: OffersScreenAppear для групп: 246 и 248, alpha=0.05
В группе 246 доля успехов: 62.08%
В группе 248 доля успехов: 60.35%
p_value=20.84% >= 5.00%
Не удалось отвергнуть нулевую гипотезу: нет статистически значимых оснований считать доли разными.
Результаты теста события: CartScreenAppear для групп: 246 и 248, alpha=0.05
В группе 246 доля успехов: 50.97%
В группе 248 доля успехов: 48.48%
p_value=7.84% >= 5.00%
Не удалось отвергнуть нулевую гипотезу: нет статистически значимых оснований считать доли разными.
Результаты теста события: PaymentScreenSuccessful для групп: 246 и 248, alpha=0.05
В группе 246 доля успехов: 48.31%
В группе 248 доля успехов: 46.55%
p_value=21.23% >= 5.00%
Не удалось отвергнуть нулевую гипотезу: нет статистически значимых оснований считать доли разными.
Сравнение с контрольной группой А2¶
Посчитаем статистическую значимость отличий между долями посетителей контрольной и экспериментальной групп А2/В по всем событиям. Сформулируем гипотезы:
H0 - нулевая гипотеза:
- различий между долями в контрольной и экспериментальной группах нет
H1 - альтернативная двусторонняя гипотеза
- различия между долями в контрольной и экспериментальной группах есть
Проверим гипотезу с помощью Z-теста для пропорций. Если вероятность ошибочно отвергнуть нулевую гипотезу p-value
окажется меньше уровня статистической значимости alpha = 5%
, то отвергнем нулевую гипотезу в пользу альтернативной.
# проверка отличий между А2/В
ztest_data(control_A2, variant_B, 0.05, 1);
Результаты теста события: MainScreenAppear для групп: 247 и 248, alpha=0.05
В группе 247 доля успехов: 98.53%
В группе 248 доля успехов: 98.27%
p_value=45.87% >= 5.00%
Не удалось отвергнуть нулевую гипотезу: нет статистически значимых оснований считать доли разными.
Результаты теста события: OffersScreenAppear для групп: 247 и 248, alpha=0.05
В группе 247 доля успехов: 60.49%
В группе 248 доля успехов: 60.35%
p_value=91.98% >= 5.00%
Не удалось отвергнуть нулевую гипотезу: нет статистически значимых оснований считать доли разными.
Результаты теста события: CartScreenAppear для групп: 247 и 248, alpha=0.05
В группе 247 доля успехов: 49.26%
В группе 248 доля успехов: 48.48%
p_value=57.86% >= 5.00%
Не удалось отвергнуть нулевую гипотезу: нет статистически значимых оснований считать доли разными.
Результаты теста события: PaymentScreenSuccessful для групп: 247 и 248, alpha=0.05
В группе 247 доля успехов: 46.08%
В группе 248 доля успехов: 46.55%
p_value=73.73% >= 5.00%
Не удалось отвергнуть нулевую гипотезу: нет статистически значимых оснований считать доли разными.
Сравнение с объединенной контрольной группой АА¶
Посчитаем статистическую значимость отличий между долями посетителей объединенной контрольной и экспериментальной групп АА/В по всем событиям. Сформулируем гипотезы:
H0 - нулевая гипотеза:
- различий между долями в объединенной контрольной и экспериментальной группах нет
H1 - альтернативная двусторонняя гипотеза
- различия между долями в объединенной контрольной и экспериментальной группах есть
Проверим гипотезу с помощью Z-теста для пропорций. Если вероятность ошибочно отвергнуть нулевую гипотезу p-value
окажется меньше уровня статистической значимости alpha = 5%
, то отвергнем нулевую гипотезу в пользу альтернативной.
# проверка отличий между АА/В
ztest_data(control_AA, variant_B, 0.05, 1);
Результаты теста события: MainScreenAppear для групп: 246247 и 248, alpha=0.05
В группе 246247 доля успехов: 98.58%
В группе 248 доля успехов: 98.27%
p_value=29.42% >= 5.00%
Не удалось отвергнуть нулевую гипотезу: нет статистически значимых оснований считать доли разными.
Результаты теста события: OffersScreenAppear для групп: 246247 и 248, alpha=0.05
В группе 246247 доля успехов: 61.28%
В группе 248 доля успехов: 60.35%
p_value=43.43% >= 5.00%
Не удалось отвергнуть нулевую гипотезу: нет статистически значимых оснований считать доли разными.
Результаты теста события: CartScreenAppear для групп: 246247 и 248, alpha=0.05
В группе 246247 доля успехов: 50.11%
В группе 248 доля успехов: 48.48%
p_value=18.18% >= 5.00%
Не удалось отвергнуть нулевую гипотезу: нет статистически значимых оснований считать доли разными.
Результаты теста события: PaymentScreenSuccessful для групп: 246247 и 248, alpha=0.05
В группе 246247 доля успехов: 47.19%
В группе 248 доля успехов: 46.55%
p_value=60.04% >= 5.00%
Не удалось отвергнуть нулевую гипотезу: нет статистически значимых оснований считать доли разными.
Выводы:
Между экспериментальной и контрольными группами есть различия между конверсиями в события, но при заданном уровне значимости
alpha = 5%
они не являются статистически значимымиВероятно не имеет смысла менять шрифт во всём приложении
Выбор уровня статистической значимости¶
При проверке 16 статистических гипотез использован уровень значимости alpha = 5%
.
Если использовать уровень значимости alpha = 10%
и провести повторные проверки статистических гипотез, то станет значимой разница только в конверсии в событие CartScreenAppear при сравнении контрольной группы А1 и экспериментальной группы В:
- в группе А1 доля успехов: 50.97%
- в группе В доля успехов: 48.48%
- p_value=7.84% >= 10%
Вывод:
- Имеет смысл использовать результаты проверки статистических гипотез с заданным уровнем ошибочно отклонить нулевую гипотезу в 5% случаев.
Итоги исследования¶
Результаты анализа воронки событий и исследования результатов A/A/B-эксперимента позволят менеджерам приложения принять правильные управленческие решения.
Установлено по результатам теста:
События в приложении выстраиваются в следующий порядок (воронку):
MainScreenAppear
->OffersScreenAppear
->CartScreenAppear
->PaymentScreenSuccessful
Больше всего пользователей теряется при переходе с первого на второй шаг событийной воронки - 38% пользователей не достигают события
OffersScreenAppear
От первого события
MainScreenAppear
до оплатыPaymentScreenSuccessful
доходит 48% пользователейРазличия в конверсии экспериментальной группы пользователей, использовавших приложение с новыми шрифтами, не являются статистически значимыми
Рекомендации:
Имеет смысл оставить исходный шрифт
Провести дополнительный А/В тест изменений, направленных на уменьшение потерь пользователей при переходе к
OffersScreenAppear