• 项目四:AB实验实战


    项目背景

    为优化落地页设计并提升转化率,某电商公司计划通过 A/B 测试验证新页面设计。该公司历年转化率平均为13%。
    目标将新页面的转化率提高2个百分点至15%。在全面推广前,公司将在一小部分用户群中进行测试,以确认新设计是否达到预期效果。

    数据集具体字段如下:

     A/B 测试的基本流程

    A/B测试,简单来说,就是同时对比两个或多个版本的网页或应用,以确定哪个版本更能实现特定的业绩目标。这里的关键步骤包括:
    1.明确实验目标和指标:首先确定实验的目的,比如我们的案例中是要验证新落地页是否能提升2%的转化率。
    2设计实验方案:包括实验的变量、持续时间、样本量以及如何合理地划分实验组和对照组,
    3.选择分析方法:常见的方法有描述性统计、假设检验等。
    4.执行实验并收集数据:这一步是整个过程中最为关键的,需要按照预定方案详细记录数据。
    5.数据分析及假设检验:通过统计方法分析数据,验证假设是否成立。
    6.得出结论及建议:根据分析结果,决定是否推广新设计。

    实验详细步骤

    1.提出假设

    对您的网站界面进行重新设计,目标是提高用户的转化率。在投入大量资源前,如何验证这一改变确实有效呢?这时,您需要设定一个科学的假设作为实验的起点。

    假设您预期这个新设计能够提升2%的转化率。根据这个目标,您可以选择不同的统计检验来支持您的决策

    单尾检验:如果您坚信新设计将优于旧版,那么这种检验可以帮助您验证“新设计的转化率高于旧版”这一具体方向的预期。

    双尾检验:如果您不确定新设计是否会带来正面或负面的影响,这种检验将验证新旧设计之间是否存在任何形式的差异。

    选择哪种检验取决于您对改变的信心和您希望从实验中得到的信息类型。

    2.分组实验

    进行AB测试的下一步是将用户随机分配到两个不同的组:
    对照组:这些用户将继续使用现有的旧版网页,
    实验组:这部分用户将体验新设计的网页,
    通过在网站上添加追踪代码,我们可以详细记录用户的行为,比如他们是否完成了购买。这些数据会被转化为简单的数字,0表示没有购买,1表示购买了产品。这样,我们就能计算出各组的平均转化率,直观地看到新旧网页设计在实际使用中的表现如何。

    3.计算样本量及实验周期

    实验样本量的确定

    在设计AB测试时,确定合适的样本量是非常重要的。根据大数定律和中心极限定理,样本量越大,我们对结果的估计就越精确。然而,增加样本量也会增加实验的成本。因此,我们需要计算出进行实验所需的最小样本量,以保证成本效益。
    通常我们可以使用以下简化的公式计算样本量:

    参数解释: 

    确定试验周期

    确定试验的运行时间也是实验设计的一个重要部分。试验周期应足够长,以便收集到足够的数据进行有效的分析,但同时也不能过长以避免不必要的资源浪费。试验周期的长短会受到预期流量、用户参与度以及样本量需求的景响。一般来说,你需要根据网站或应用的用户流量来预估实验组和对照组能在多长时间内达到所需的样本量。


    在此案例中PA=13%,PB=15%,每个组所需的最小样本量为计算过程如下:

    在AB测试和其他统计试验中,当我们计算出“每组所需的最小样本量”时,通常这个数字指的是单个组(比如对照组或实验组)所需的样本量。如果我们有两个组(一个对照组和一个实验组),那么总的参与者数量将是每组所需样本量的两倍。


    所以根据上面最小样本量的计算,我们知道此次AB测试至少需要4716*2=9432个用户参与测试,假如该落地页以往每天的平均浏览量为x,则实验周期至少需要的天数为:

    试验周期=9432/x 

    4.实施AB测试并收集数据

    通过第一方或者第三方工具进行测试,收集用户行为数据。

    5.数据分析及假设检验

    代码实现:ab实验.ipynb

    具体步骤如下:

    1. # 导入依赖
    2. import numpy as np
    3. import pandas as pd
    4. import scipy.stats as stats
    5. import statsmodels.stats.api as sms
    6. import matplotlib as mpl
    7. import matplotlib.pyplot as plt
    8. import seaborn as sns

    计算样本量

    1. # 设置基本参数
    2. baseline_conversion_rate = 0.13 # 基准转化率
    3. expected_conversion_rate = 0.15 # 期望转化率
    4. statistical_power = 0.8 # 统计功效
    5. significance_level = 0.05 # 显著性水平
    6. control_to_treatment_ratio = 1 # 控制组与处理组的比例
    7. # 计算效果量
    8. effect_size = sms.proportion_effectsize(baseline_conversion_rate, expected_conversion_rate)
    9. print(f"效果量为: {effect_size}")
    10. # 计算所需的样本量
    11. required_sample_size = sms.NormalIndPower().solve_power(
    12. effect_size,
    13. power=statistical_power,
    14. alpha=significance_level,
    15. ratio=control_to_treatment_ratio
    16. )
    17. # 向上取整以确保有足够的样本量
    18. import math
    19. required_sample_size = math.ceil(required_sample_size)
    20. print(f"需要的最小样本量为: {required_sample_size}")

    结果如下:

    效果量为: -0.0576728617308947
    需要的最小样本量为: 4720

    导入数据集

    df  = pd.read_csv("ab_data.csv")
    df.head()

    df.info()

     得到数据集 "ab_data.csv" 的字段描述,一共包含五个列:

    数据列用途有效值
    user_id用户唯一IDInt64值
    timestamp用户访问网页的时间戳-
    group在当前的A/B实验中,用户被分为两个主要组别。control组的用户应该访问old_page;treatment组的用户则对应new_page。但是,初始数据中存在一些不准确的行,例如control组用户访问了new_page。['control', 'treatment']
    landing_page表示用户访问的是旧页面还是新页面。['old_page', 'new_page']
    converted表示用户是否决定为公司的产品付费。这里,1表示用户购买了产品。[0, 1]

     数据清洗

    1. # 1. 检查缺失值
    2. missing_values = df.isnull().sum()
    3. print("每一行的缺失数量为:\n", missing_values)
    4. # 2. 检查并处理重复的行
    5. duplicate_rows = df.duplicated().sum()
    6. print(f"重复行数量为: {duplicate_rows}")
    7. # 删除重复行
    8. df.drop_duplicates(inplace=True)
    9. # 3. 检查"user_id"的重复值
    10. user_id_duplicates = df["user_id"].duplicated().sum()
    11. print(f"重复的user_id数量为: {user_id_duplicates}")
    12. # 显示重复的"user_id"
    13. duplicate_user_ids = df[df["user_id"].duplicated()]["user_id"]
    14. print("重复的user_id为:\n", duplicate_user_ids)
    15. # 选取一个重复的"user_id"进行检查
    16. sample_user_id = 773192 # 示例用户ID
    17. print(f" {sample_user_id}:\n", df[df["user_id"] == sample_user_id])
    18. # 存储所有重复的"user_id"
    19. duplicate_user_ids_to_delete = df[df["user_id"].duplicated(keep=False)]["user_id"].unique()
    20. print("移除所有的重复user_id:\n", duplicate_user_ids_to_delete)
    21. # 删除所有重复的"user_id",保留第一次出现
    22. df_new = df.drop_duplicates(subset='user_id', keep='first')
    23. print("一处重复user_id后的数据集:\n", df_new)

    输入以下:

    每一行的缺失数量为:
     user_id         0
    timestamp       0
    group           0
    landing_page    0
    converted       0
    dtype: int64
    重复行数量为: 0
    重复的user_id数量为: 3894
    重复的user_id为:
     2656      698120
    2893      773192
    7500      899953
    8036      790934
    10218     633793
               ...  
    294308    905197
    294309    787083
    294328    641570
    294331    689637
    294355    744456
    Name: user_id, Length: 3894, dtype: int64
     773192:
           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
    移除所有的重复user_id:
     [767017 656468 773693 ... 921581 742781 638376]
    一处重复user_id后的数据集:
             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
    ...         ...                         ...        ...          ...        ...
    294473   751197  2017-01-03 22:28:38.630509    control     old_page          0
    294474   945152  2017-01-12 00:51:57.078372    control     old_page          0
    294475   734608  2017-01-22 11:45:03.439544    control     old_page          0
    294476   697314  2017-01-15 01:20:28.957438    control     old_page          0
    294477   715931  2017-01-16 12:40:24.467417  treatment     new_page          0
    
    [290584 rows x 5 columns]
    1. # 1. 将时间戳转换为日期格式
    2. df_new['date'] = pd.to_datetime(df_new['timestamp']).dt.date
    3. # 打印日期列以检查
    4. print("转换后的日期数据:\n", df_new['date'])
    5. # 2. 检查不同日期的数量
    6. unique_dates_count = len(df_new['date'].unique())
    7. print(f"不同日期的天数:{unique_dates_count}")
    8. # 3. 检查DataFrame的信息,确保数据类型正确对应
    9. print("数据框的详细信息:")
    10. df_new.info()
    11. # 4. 提取并打印日期部分,以确保每条记录正确匹配
    12. df_new['date_from_timestamp'] = df_new['timestamp'].str.split(" ").str[0]
    13. # 打印提取的日期部分
    14. print("从时间戳中提取的日期部分:\n", df_new['date_from_timestamp'])

    得到以下输出:

    转换后的日期数据:
     0         2017-01-21
    1         2017-01-12
    2         2017-01-11
    3         2017-01-08
    4         2017-01-21
                 ...    
    294473    2017-01-03
    294474    2017-01-12
    294475    2017-01-22
    294476    2017-01-15
    294477    2017-01-16
    Name: date, Length: 290584, dtype: object
    不同日期的天数:23
    数据框的详细信息:
    
    Index: 290584 entries, 0 to 294477
    Data columns (total 6 columns):
     #   Column        Non-Null Count   Dtype 
    ---  ------        --------------   ----- 
     0   user_id       290584 non-null  int64 
     1   timestamp     290584 non-null  object
     2   group         290584 non-null  object
     3   landing_page  290584 non-null  object
     4   converted     290584 non-null  int64 
     5   date          290584 non-null  object
    dtypes: int64(2), object(4)
    memory usage: 15.5+ MB
    从时间戳中提取的日期部分:
     0         2017-01-21
    1         2017-01-12
    2         2017-01-11
    3         2017-01-08
    4         2017-01-21
                 ...    
    294473    2017-01-03
    294474    2017-01-12
    294475    2017-01-22
    294476    2017-01-15
    294477    2017-01-16
    Name: date_from_timestamp, Length: 290584, dtype: object

    1. # 确保control组每个用户看到的都是old_page,treatment组看到的都是new_page
    2. # 移除不一致的数据
    3. df_clean = df_new[
    4. ((df_new["group"] == "treatment") & (df_new["landing_page"] == "new_page")) |
    5. ((df_new["group"] == "control") & (df_new["landing_page"] == "old_page"))
    6. ]
    7. # 再次生成交叉表以验证清理后的情况
    8. crosstab_result_clean = pd.crosstab(df_clean["group"], df_clean["landing_page"])
    9. print("清理后的交叉表结果:\n", crosstab_result_clean)
    10. # 计算被移除的数据量
    11. removed_entries = len(df_new) - len(df_clean)
    12. print(f"被移除的数据条目数:{removed_entries}")

     得到以下结果:

    清理后的交叉表结果:
     landing_page  new_page  old_page
    group                           
    control              0    144226
    treatment       144314         0
    被移除的数据条目数:2044

    抽样

    根据前面最小样本量的计算,我们至少需要每组4720个样本,这里我们选择每组抽样5000个(实际工作中不需要抽样这一步) 

    df_new[df_new["group"] == "control"].sample(n = 5000, random_state = 22)

     数据可视化

    1. # 加载数据
    2. #ab_test = pd.read_csv("ab_data.csv")
    3. # 设置中文字体和防止符号显示问题
    4. plt.rcParams["font.family"] = "SimHei"
    5. plt.rcParams["axes.unicode_minus"] = False
    6. # 计算每组的转化率和标准差
    7. conversion_rates = df_new.groupby("group")["converted"].agg(['mean', 'std']).reset_index()
    8. conversion_rates.columns = ["group", "conversion_rate", "std_deviation"]
    9. # 创建可视化
    10. plt.figure(figsize=(8,6), dpi=60)
    11. sns.barplot(x="group", y="conversion_rate", data=conversion_rates, errorbar=None)
    12. # 设置y轴的限制和标签/标题大小
    13. plt.ylim(0, 0.17)
    14. plt.title("分组转化率", fontsize=20)
    15. plt.xlabel("分组", fontsize=15)
    16. plt.ylabel("转化(比例)", fontsize=15)
    17. # 显示图形
    18. plt.show()

    从上面的统计数据来看,新旧两版落地页的表现结果非常相近,相比于旧版落地页,新版落地页的转化率略微好一点点。那么,这种差异在统计学上显著么?我们可以直接说,新版落地页更好么?

    假设检验

    我们分析的最后一步就是假设检验了。那么,具体选择哪一种假设检验呢?在统计学中,当样本容量较大时(一般是大于30),我们可以使用Z检验或者t检验。

    在这个案例中,由于我们的样本非常大,所以我们使用Z检验。Python中的statsmodels.stats.proportion模块可以来计算P值和置信区间:

    from statsmodels.stats.proportion import proportions_ztest, proportion_confint
    1. # 分离控制组和实验组的转化数据
    2. control_results = df_new[df_new['group'] == 'control']['converted']
    3. treatment_results = df_new[df_new['group'] == 'treatment']['converted']
    1. # 计算每组的样本数量
    2. n_con = control_results.count()
    3. n_treat = treatment_results.count()
    4. # 计算每组的成功次数
    5. successes = [control_results.sum(), treatment_results.sum()]
    6. nobs = [n_con, n_treat]
    7. # 进行Z检验
    8. z_stat, pval = proportions_ztest(successes, nobs=nobs)
    9. # 计算95%置信区间
    10. (lower_con, lower_treat), (upper_con, upper_treat) = proportion_confint(successes, nobs=nobs, alpha=0.05)
    11. # 中文输出结果
    12. print(f'Z统计量: {z_stat:.2f}')
    13. print(f'P值: {pval:.3f}')
    14. print(f'控制组95%置信区间: [{lower_con:.3f}, {upper_con:.3f}]')
    15. print(f'实验组95%置信区间: [{lower_treat:.3f}, {upper_treat:.3f}]')

    得出结果如下: 

    Z统计量: 1.24
    P值: 0.216
    控制组95%置信区间: [0.119, 0.122]
    实验组95%置信区间: [0.117, 0.121]

    结果解释

    1. Z统计量 (1.24): Z统计量表示控制组与实验组之间转化率差异的标准偏差数量。数值1.24表明实验组的转化率略高于控制组,但差异不是非常显著。

    2. P值 (0.216): P值用于评估观察到的数据在零假设(即两组之间没有差异)成立的情况下出现的概率。在这里,P值为0.216,远高于常用的显著性水平0.05。这意味着我们没有足够的证据拒绝零假设,即两组之间的转化率没有显著差异。

    3. 95%置信区间:

      • 控制组的95%置信区间为[0.119, 0.122],这意味着如果重复实验多次,大约95%的实验中,控制组的转化率将落在这个区间内。
      • 实验组的95%置信区间为[0.117, 0.121],情况与控制组相似。

    6.结论与建议

    A/B测试结果的解读

    在最近完成的A/B测试中,我们对比了新旧两版网页设计的表现。测试的核心是检验新版页面是否能显著提升用户的转化率。转化率,简单来说,就是访问者执行我们期望行为(如购买、注册)的比例。
    测试结果的统计显著性由P值衡量,而此次我们得到的P值为0.216。在统计学中,如果P值低于预设阈值阿尔法a (本测试中为0.05),我们才认为结果具有统计显著性。显然,P值0.216远高于0.05,意味着新版网页的效果与旧版没有显著差异。换句话说,数据没有给我们足够的信心去断言新设计带来了改进。


    关于置信区间的讨论
    进一步分析新版页面的性能,我们观察到其置信区间为[0.117,0.121]。这个区间提供了关于新页面转化率的可能范围,而且它涵盖了现行转化率基准线13%。这一发现表明,尽管新页面未能显著提升转化率,但它至少保持了业绩稳定。
    然而,我们的目标是将转化率提高至15%,而当前的置信区间并未触及这一目标。这提示我们,如果要达到15%的转化率目标,单靠当前的页面设计可能是不够的。

    下一步的策略

    虽然这次的A/B测试结果可能令人略感失望,但每一次数据的反馈都是向前迈进的一步。以下是我们可以考虑的几个方向:
    1.页面设计再优化:根据用户反馈和行为数据进一步细化设计元素
    2.进行更多的A/B测试:测试其他变量组合,看是否能找到更有效的解决方案。
    3.深入分析用户行为:使用高级分析工具,如热图和行为流分析,以更深入地理解用户与页面的互动。

  • 相关阅读:
    人工智能、深度学习、机器学习常见面试题121~140
    RHCE---正则表达式
    logback+MQ+Logstash 日志收集
    大数据-ClickHouse技术二(数据库引擎)
    《C++设计模式》——创建型
    【NOI模拟赛】纸老虎博弈(博弈论SG函数,长链剖分)
    Spring Boot自动装配原理超详细解析
    linux 部署elasticsearch 权限不足 报错问题
    政务场景下-基于OAuth2.0 统一认证中心架构设计
    Scroll zkEvm技术设计全面思考
  • 原文地址:https://blog.csdn.net/zfyzfw/article/details/139232334