|
|
|
|
|
<!--
|
|
|
|
|
|
* @Author: qinzhenpen qzp1807@126.com
|
|
|
|
|
|
* @Date: 2025-04-30 16:34:04
|
|
|
|
|
|
* @LastEditors: qinzhenpen qzp1807@126.com
|
|
|
|
|
|
* @LastEditTime: 2025-05-16 17:32:57
|
|
|
|
|
|
* @FilePath: \e-commerce-internet\src\views\test\promotion.vue
|
|
|
|
|
|
* @Description: A/B testing component with ECharts boxplots using rawData1 directly
|
|
|
|
|
|
-->
|
|
|
|
|
|
<template>
|
|
|
|
|
|
<div class="promotion app-container">
|
|
|
|
|
|
<div class="handren">
|
|
|
|
|
|
<div class="left">
|
|
|
|
|
|
<el-button>A/B测试</el-button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="right">
|
|
|
|
|
|
<el-button @click="dialogVisible = true" type="primary" id="brtn">任务背景与目标</el-button>
|
|
|
|
|
|
<popModel :showModel="dialogVisible" title="任务背景:" @closePop="dialogVisible = false">
|
|
|
|
|
|
<template v-slot:content>
|
|
|
|
|
|
<p style="font-size: 1rem">
|
|
|
|
|
|
电商产品推广测试是电商运营中的重要环节,它有助于商家了解产品在市场上的表现,并据此优化推广策略。
|
|
|
|
|
|
<br />
|
|
|
|
|
|
<br />
|
|
|
|
|
|
测试目的
|
|
|
|
|
|
<br />
|
|
|
|
|
|
评估产品市场潜力:通过推广测试,商家可以评估产品在目标市场中的受欢迎程度,以及潜在的销售潜力。
|
|
|
|
|
|
<br />
|
|
|
|
|
|
优化推广策略:根据测试数据,商家可以调整推广方式、渠道选择、广告文案等,以提高推广效果。
|
|
|
|
|
|
<br />
|
|
|
|
|
|
提升用户体验:通过测试用户对产品的反应,商家可以发现并改进产品中的不足之处,从而提升用户体验。
|
|
|
|
|
|
<br />
|
|
|
|
|
|
<br />
|
|
|
|
|
|
任务目标:
|
|
|
|
|
|
<br />
|
|
|
|
|
|
<br />
|
|
|
|
|
|
使用A/B测试法,根据曝光量、点击量、转化量、广告费用、订单金额等数据,评估A版广告和B版广告优劣,根据分析结果进行问题定位,制定相应对策。
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</popModel>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<section class="intro">
|
|
|
|
|
|
<p>在电商产品推广测试中,A/B测试(也称为分裂测试)是一种科学的方法,用于比较两个或多个版本的广告、网页或其他营销元素,以确定哪个版本在给定目标方面表现更好。</p>
|
|
|
|
|
|
<p>
|
|
|
|
|
|
<strong>A/B测试的步骤:</strong>
|
|
|
|
|
|
<br />
|
|
|
|
|
|
- 明确目标:确定A/B测试的目标,例如提升点击率、转化率或投资回报率。
|
|
|
|
|
|
<br />
|
|
|
|
|
|
- 选择变量:选择需要测试的变量,例如广告文案、图片、按钮颜色等。
|
|
|
|
|
|
<br />
|
|
|
|
|
|
- 创建实验组和对照组:将用户随机分配到实验组和对照组,确保样本量足够大。
|
|
|
|
|
|
<br />
|
|
|
|
|
|
- 施加影响:对实验组施加影响(如不同的广告创意),而对照组保持不变。
|
|
|
|
|
|
<br />
|
|
|
|
|
|
- 收集数据:收集实验组和对照组的数据,包括点击率、转化率、停留时间等指标。
|
|
|
|
|
|
<br />
|
|
|
|
|
|
- 分析数据:对收集的数据进行分析,以确定新运营策略是否优于旧运营策略。
|
|
|
|
|
|
<br />
|
|
|
|
|
|
- 优化策略:根据分析结果,优化运营策略,以提升运营效果。
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
|
|
<section class="case-study">
|
|
|
|
|
|
<div class="case-backtitle">
|
|
|
|
|
|
<img src="@/assets/images/步骤.png" alt="实验实训图标" />
|
|
|
|
|
|
<span>实验实训</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<p>假设京东某电子产品店铺进行新产品推广测试,设计以下A、B两个版本广告,5个月时间两个版本广告的点击量、曝光量、转化量、订单金额和广告费用数据如下所示:</p>
|
|
|
|
|
|
<el-table :data="rawData" border class="data-table">
|
|
|
|
|
|
<el-table-column prop="version" label="广告版本" align="center" />
|
|
|
|
|
|
<el-table-column prop="clicks" label="点击量" align="center" />
|
|
|
|
|
|
<el-table-column prop="exposure" label="曝光量" align="center" />
|
|
|
|
|
|
<el-table-column prop="conversions" label="转化量" align="center" />
|
|
|
|
|
|
<el-table-column prop="orderAmount" label="订单金额" align="center" />
|
|
|
|
|
|
<el-table-column prop="adCost" label="广告费用" align="center" />
|
|
|
|
|
|
</el-table>
|
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
|
|
<section class="analysis">
|
|
|
|
|
|
<h3>数据分析与可视化</h3>
|
|
|
|
|
|
<p class="formulas">
|
|
|
|
|
|
<span>点击率 = 点击量 / 曝光量</span>
|
|
|
|
|
|
<span>转化率 = 转化量 / 点击量</span>
|
|
|
|
|
|
<span>投资回报率 = (订单金额 - 广告费用) / 广告费用</span>
|
|
|
|
|
|
</p>
|
|
|
|
|
|
<p>请根据上述公式计算A、B两个版本的点击率、转化率、投资回报率指标数据(计算结果保留4位小数),根据计算结果绘制点击率对比、转化率对比、投资回报率对比、订单金额对比箱线图</p>
|
|
|
|
|
|
<el-table :data="rawData1" border class="data-table">
|
|
|
|
|
|
<el-table-column prop="version" label="广告版本" align="center" />
|
|
|
|
|
|
<el-table-column prop="clicks" label="点击率" align="center">
|
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
|
<el-input v-model.number="row.clicks" placeholder="请输入点击率" />
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column prop="exposure" label="转化率" align="center">
|
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
|
<el-input v-model.number="row.exposure" placeholder="请输入转化率" />
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column prop="conversions" label="投资回报率" align="center">
|
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
|
<el-input v-model.number="row.conversions" placeholder="请输入投资回报率" />
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column prop="orderAmount" label="订单金额" align="center" />
|
|
|
|
|
|
</el-table>
|
|
|
|
|
|
<el-button @click="initializeChart">点击绘制箱线图</el-button>
|
|
|
|
|
|
<el-button @click="runResultShow = true">查看代码</el-button>
|
|
|
|
|
|
<el-button @click="go">跳转到Python在线编程环境</el-button>
|
|
|
|
|
|
<div ref="chartContainer" class="chart-container"></div>
|
|
|
|
|
|
</section>
|
|
|
|
|
|
<section class="statistical-test">
|
|
|
|
|
|
<h3>统计显著性检验(保留4位小数):</h3>
|
|
|
|
|
|
<el-table :data="testResults" border class="data-table" style="width: 50%">
|
|
|
|
|
|
<el-table-column prop="testName" label="t检验结果" align="center" />
|
|
|
|
|
|
<el-table-column prop="statistic" label="t统计量" align="center">
|
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
|
<el-input v-model="row.statistic" placeholder="请输入t统计量" />
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column prop="pValue" label="p值" align="center">
|
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
|
<el-input v-model="row.pValue" placeholder="请输入p值" />
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
</el-table>
|
|
|
|
|
|
</section>
|
|
|
|
|
|
<section class="results">
|
|
|
|
|
|
<h3>A/B测试结果分析</h3>
|
|
|
|
|
|
<el-input v-model="analysisText" :autosize="{ minRows: 11, maxRows: 11 }" type="textarea" placeholder="请输入A/B测试结果分析" />
|
|
|
|
|
|
</section>
|
|
|
|
|
|
<div class="taskfooter">
|
|
|
|
|
|
<el-button @click="submitTask">提交任务</el-button>
|
|
|
|
|
|
<el-button @click="resetTask">重新实训</el-button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<pop-model :showModel="runResultShow" title="设置代码" @closePop="closeCode">
|
|
|
|
|
|
<template v-slot:content>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<codemirror :code="code"></codemirror>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</pop-model>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup >
|
|
|
|
|
|
import * as analysisAPI from "@/api/market-demand-mining.js";
|
|
|
|
|
|
import * as echarts from "echarts";
|
|
|
|
|
|
import codemirror from "@/components/codemirror/index.vue";
|
|
|
|
|
|
import popModel from "@/views/components/popModal.vue";
|
|
|
|
|
|
const { proxy } = getCurrentInstance();
|
|
|
|
|
|
import useUserStore from "@/store/modules/user";
|
|
|
|
|
|
const userStore = useUserStore();
|
|
|
|
|
|
import jStat from "jstat";
|
|
|
|
|
|
import { ref } from "vue";
|
|
|
|
|
|
const runResultShow = ref(false);
|
|
|
|
|
|
const dialogVisible = ref(false);
|
|
|
|
|
|
// 图表显示隐藏
|
|
|
|
|
|
const code = ref(
|
|
|
|
|
|
`
|
|
|
|
|
|
import pandas as pd
|
|
|
|
|
|
import numpy as np
|
|
|
|
|
|
import scipy.stats as stats
|
|
|
|
|
|
import matplotlib.pyplot as plt
|
|
|
|
|
|
plt.rcParams['font.sans-serif'] = ['SimHei']
|
|
|
|
|
|
import seaborn as sns
|
|
|
|
|
|
# 模拟数据
|
|
|
|
|
|
data = {
|
|
|
|
|
|
'广告版本': ['A', 'A', 'A', 'A', 'A', 'B', 'B', 'B', 'B', 'B'],
|
|
|
|
|
|
'点击量': [120, 150, 130, 140, 160, 180, 200, 190, 210, 220],
|
|
|
|
|
|
'曝光量': [1000, 1200, 1100, 1300, 1400, 1500, 1600, 1700, 1800, 2000],
|
|
|
|
|
|
'转化量': [20, 25, 22, 24, 28, 30, 35, 32, 38, 40],
|
|
|
|
|
|
'订单金额': [2000, 2500, 2200, 2400, 2800, 3000, 3500, 3200, 3800, 4000],
|
|
|
|
|
|
'广告费用': [500, 600, 550, 650, 700, 750, 800, 780, 850, 900]
|
|
|
|
|
|
}
|
|
|
|
|
|
df = pd.DataFrame(data)
|
|
|
|
|
|
# 计算关键指标
|
|
|
|
|
|
df['点击率'] = df['点击量'] / df['曝光量']
|
|
|
|
|
|
df['转化率'] = df['转化量'] / df['点击量']
|
|
|
|
|
|
df['投资回报率'] = (df['订单金额'] - df['广告费用']) / df['广告费用']
|
|
|
|
|
|
# 数据可视化
|
|
|
|
|
|
plt.figure(figsize=(12, 8))
|
|
|
|
|
|
# 点击率
|
|
|
|
|
|
plt.subplot(2, 2, 1)
|
|
|
|
|
|
sns.boxplot(x='广告版本', y='点击率', data=df)
|
|
|
|
|
|
plt.title('点击率对比')
|
|
|
|
|
|
# 转化率
|
|
|
|
|
|
plt.subplot(2, 2, 2)
|
|
|
|
|
|
sns.boxplot(x='广告版本', y='转化率', data=df)
|
|
|
|
|
|
plt.title('转化率对比')
|
|
|
|
|
|
# 投资回报率
|
|
|
|
|
|
plt.subplot(2, 2, 3)
|
|
|
|
|
|
sns.boxplot(x='广告版本', y='投资回报率', data=df)
|
|
|
|
|
|
plt.title('投资回报率对比')
|
|
|
|
|
|
# 订单金额
|
|
|
|
|
|
plt.subplot(2, 2, 4)
|
|
|
|
|
|
sns.boxplot(x='广告版本', y='订单金额', data=df)
|
|
|
|
|
|
plt.title('订单金额对比')
|
|
|
|
|
|
plt.tight_layout()
|
|
|
|
|
|
plt.show()
|
|
|
|
|
|
# 提取A组和B组数据
|
|
|
|
|
|
a_group = df[df['广告版本'] == 'A']
|
|
|
|
|
|
b_group = df[df['广告版本'] == 'B']
|
|
|
|
|
|
# 点击率的t检验
|
|
|
|
|
|
t_stat_click, p_val_click = stats.ttest_ind(a_group['点击率'], b_group['点击率'])
|
|
|
|
|
|
print(f"点击率t检验结果: t统计量={t_stat_click}, p值={p_val_click}")
|
|
|
|
|
|
# 转化率的t检验
|
|
|
|
|
|
t_stat_conversion, p_val_conversion = stats.ttest_ind(a_group['转化率'], b_group['转化率'])
|
|
|
|
|
|
print(f"转化率t检验结果: t统计量={t_stat_conversion}, p值={p_val_conversion}")
|
|
|
|
|
|
# 投资回报率的t检验
|
|
|
|
|
|
t_stat_roi, p_val_roi = stats.ttest_ind(a_group['投资回报率'], b_group['投资回报率'])
|
|
|
|
|
|
print(f"投资回报率t检验结果: t统计量={t_stat_roi}, p值={p_val_roi}")
|
|
|
|
|
|
# 订单金额的t检验
|
|
|
|
|
|
t_stat_revenue, p_val_revenue = stats.ttest_ind(a_group['订单金额'], b_group['订单金额'])
|
|
|
|
|
|
print(f"订单金额t检验结果: t统计量={t_stat_revenue}, p值={p_val_revenue}")
|
|
|
|
|
|
# 设置显著性水平
|
|
|
|
|
|
alpha = 0.05
|
|
|
|
|
|
# 评估结果
|
|
|
|
|
|
def evaluate_result(p_value, metric):
|
|
|
|
|
|
if p_value < alpha:
|
|
|
|
|
|
print(f"{metric}差异显著,B组效果优于A组。")
|
|
|
|
|
|
else:
|
|
|
|
|
|
print(f"{metric}差异不显著,无法确定B组效果优于A组。")
|
|
|
|
|
|
evaluate_result(p_val_click, "点击率")
|
|
|
|
|
|
evaluate_result(p_val_conversion, "转化率")
|
|
|
|
|
|
evaluate_result(p_val_roi, "投资回报率")
|
|
|
|
|
|
evaluate_result(p_val_revenue, "订单金额")
|
|
|
|
|
|
# 基于结果优化策略
|
|
|
|
|
|
if p_val_click < alpha and p_val_conversion < alpha and p_val_roi < alpha:
|
|
|
|
|
|
print("B组在点击率、转化率和投资回报率方面均优于A组,建议全面推广B组广告版本。")
|
|
|
|
|
|
elif p_val_click < alpha:
|
|
|
|
|
|
print("B组在点击率方面优于A组,建议优化广告文案或图片以提高点击率。")
|
|
|
|
|
|
elif p_val_conversion < alpha:
|
|
|
|
|
|
print("B组在转化率方面优于A组,建议优化产品页面或购买流程以提高转化率。")
|
|
|
|
|
|
elif p_val_roi < alpha:
|
|
|
|
|
|
print("B组在投资回报率方面优于A组,建议调整广告预算分配以提高投资回报率。")
|
|
|
|
|
|
else:
|
|
|
|
|
|
print("B组效果不显著,建议重新设计测试或优化测试变量。")`
|
|
|
|
|
|
);
|
|
|
|
|
|
// Constants
|
|
|
|
|
|
const TABLE_HEADER_STYLE = {
|
|
|
|
|
|
color: "#fff",
|
|
|
|
|
|
fontSize: "14px",
|
|
|
|
|
|
backgroundColor: "#6A7DE9 !important",
|
|
|
|
|
|
};
|
|
|
|
|
|
const CHART_COLOR = "#4682B4";
|
|
|
|
|
|
const ALPHA = 0.05;
|
|
|
|
|
|
// Reactive State
|
|
|
|
|
|
const chartContainer = ref(null);
|
|
|
|
|
|
const chartLoading = ref(false);
|
|
|
|
|
|
const analysisText = ref("");
|
|
|
|
|
|
const rawData = ref([
|
|
|
|
|
|
{ version: "A (1月)", clicks: 123, exposure: 1042, conversions: 21, orderAmount: 2059, adCost: 512 },
|
|
|
|
|
|
{ version: "A (2月)", clicks: 159, exposure: 1208, conversions: 25, orderAmount: 2515, adCost: 632 },
|
|
|
|
|
|
{ version: "A (3月)", clicks: 131, exposure: 1137, conversions: 22, orderAmount: 2207, adCost: 552 },
|
|
|
|
|
|
{ version: "A (4月)", clicks: 144, exposure: 1321, conversions: 24, orderAmount: 2436, adCost: 653 },
|
|
|
|
|
|
{ version: "A (5月)", clicks: 167, exposure: 1423, conversions: 28, orderAmount: 2853, adCost: 709 },
|
|
|
|
|
|
{ version: "B (1月)", clicks: 183, exposure: 1505, conversions: 30, orderAmount: 3079, adCost: 751 },
|
|
|
|
|
|
{ version: "B (2月)", clicks: 209, exposure: 1609, conversions: 35, orderAmount: 3507, adCost: 819 },
|
|
|
|
|
|
{ version: "B (3月)", clicks: 194, exposure: 1719, conversions: 32, orderAmount: 3215, adCost: 784 },
|
|
|
|
|
|
{ version: "B (4月)", clicks: 218, exposure: 1851, conversions: 38, orderAmount: 3831, adCost: 853 },
|
|
|
|
|
|
{ version: "B (5月)", clicks: 220, exposure: 2032, conversions: 40, orderAmount: 4023, adCost: 921 },
|
|
|
|
|
|
]);
|
|
|
|
|
|
const rawData1 = ref([
|
|
|
|
|
|
{ version: "A (1月)", clicks: "", exposure: "", conversions: "", orderAmount: 2059 },
|
|
|
|
|
|
{ version: "A (2月)", clicks: "", exposure: "", conversions: "", orderAmount: 2515 },
|
|
|
|
|
|
{ version: "A (3月)", clicks: "", exposure: "", conversions: "", orderAmount: 2207 },
|
|
|
|
|
|
{ version: "A (4月)", clicks: "", exposure: "", conversions: "", orderAmount: 2436 },
|
|
|
|
|
|
{ version: "A (5月)", clicks: "", exposure: "", conversions: "", orderAmount: 2853 },
|
|
|
|
|
|
{ version: "B (1月)", clicks: "", exposure: "", conversions: "", orderAmount: 3079 },
|
|
|
|
|
|
{ version: "B (2月)", clicks: "", exposure: "", conversions: "", orderAmount: 3507 },
|
|
|
|
|
|
{ version: "B (3月)", clicks: "", exposure: "", conversions: "", orderAmount: 3215 },
|
|
|
|
|
|
{ version: "B (4月)", clicks: "", exposure: "", conversions: "", orderAmount: 3831 },
|
|
|
|
|
|
{ version: "B (5月)", clicks: "", exposure: "", conversions: "", orderAmount: 4023 },
|
|
|
|
|
|
]);
|
|
|
|
|
|
const testResults = ref([
|
|
|
|
|
|
{ testName: "点击率", statistic: "", pValue: "" },
|
|
|
|
|
|
{ testName: "转化率", statistic: "", pValue: "" },
|
|
|
|
|
|
{ testName: "投资回报率", statistic: "", pValue: "" },
|
|
|
|
|
|
{ testName: "订单金额", statistic: "", pValue: "" },
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
// Computed Metrics for rawData (to display CTR, CVR, ROI in the first table)
|
|
|
|
|
|
function calculateMetrics(data) {
|
|
|
|
|
|
return data.map((item) => ({
|
|
|
|
|
|
...item,
|
|
|
|
|
|
ctr: item.exposure ? item.clicks / item.exposure : 0,
|
|
|
|
|
|
cvr: item.clicks ? item.conversions / item.clicks : 0,
|
|
|
|
|
|
roi: item.adCost ? (item.orderAmount - item.adCost) / item.adCost : 0,
|
|
|
|
|
|
}));
|
|
|
|
|
|
}
|
|
|
|
|
|
// Chart Logic
|
|
|
|
|
|
function initializeChart() {
|
|
|
|
|
|
if (!chartContainer.value) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
chartLoading.value = true;
|
|
|
|
|
|
try {
|
|
|
|
|
|
const chart = echarts.init(chartContainer.value);
|
|
|
|
|
|
|
|
|
|
|
|
// Group data by version (A or B)
|
|
|
|
|
|
const groupedData = {
|
|
|
|
|
|
A: { clicks: [], exposure: [], conversions: [], orderAmount: [] },
|
|
|
|
|
|
B: { clicks: [], exposure: [], conversions: [], orderAmount: [] },
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
rawData1.value.forEach((item) => {
|
|
|
|
|
|
const version = item.version.startsWith("A") ? "A" : "B";
|
|
|
|
|
|
// Convert user input to numbers, default to 0 if empty or invalid
|
|
|
|
|
|
groupedData[version].clicks.push(Number(item.clicks) || 0);
|
|
|
|
|
|
groupedData[version].exposure.push(Number(item.exposure) || 0);
|
|
|
|
|
|
groupedData[version].conversions.push(Number(item.conversions) || 0);
|
|
|
|
|
|
groupedData[version].orderAmount.push(Number(item.orderAmount) || 0);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// Prepare boxplot data and calculate outliers
|
|
|
|
|
|
const boxData = {
|
|
|
|
|
|
clicks: [prepareBoxPlotData(groupedData.A.clicks), prepareBoxPlotData(groupedData.B.clicks)],
|
|
|
|
|
|
exposure: [prepareBoxPlotData(groupedData.A.exposure), prepareBoxPlotData(groupedData.B.exposure)],
|
|
|
|
|
|
conversions: [prepareBoxPlotData(groupedData.A.conversions), prepareBoxPlotData(groupedData.B.conversions)],
|
|
|
|
|
|
orderAmount: [prepareBoxPlotData(groupedData.A.orderAmount), prepareBoxPlotData(groupedData.B.orderAmount)],
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const outlierData = {
|
|
|
|
|
|
clicks: [calcOutliers(groupedData.A.clicks), calcOutliers(groupedData.B.clicks)],
|
|
|
|
|
|
exposure: [calcOutliers(groupedData.A.exposure), calcOutliers(groupedData.B.exposure)],
|
|
|
|
|
|
conversions: [calcOutliers(groupedData.A.conversions), calcOutliers(groupedData.B.conversions)],
|
|
|
|
|
|
orderAmount: [calcOutliers(groupedData.A.orderAmount), calcOutliers(groupedData.B.orderAmount)],
|
|
|
|
|
|
};
|
|
|
|
|
|
// ECharts configuration with adjusted sizes
|
|
|
|
|
|
const option = {
|
|
|
|
|
|
title: [
|
|
|
|
|
|
{ text: "点击量对比", left: "25%", top: "0%", textStyle: { fontSize: 16 } },
|
|
|
|
|
|
{ text: "曝光量对比", left: "70%", top: "0%", textStyle: { fontSize: 16 } },
|
|
|
|
|
|
{ text: "转化量对比", left: "25%", top: "55%", textStyle: { fontSize: 16 } },
|
|
|
|
|
|
{ text: "订单金额对比", left: "70%", top: "55%", textStyle: { fontSize: 16 } },
|
|
|
|
|
|
],
|
|
|
|
|
|
grid: [
|
|
|
|
|
|
{ left: "10%", top: "10%", width: "35%", height: "35%" },
|
|
|
|
|
|
{ left: "55%", top: "10%", width: "35%", height: "35%" },
|
|
|
|
|
|
{ left: "10%", top: "60%", width: "35%", height: "35%" },
|
|
|
|
|
|
{ left: "55%", top: "60%", width: "35%", height: "35%" },
|
|
|
|
|
|
],
|
|
|
|
|
|
xAxis: [
|
|
|
|
|
|
{ gridIndex: 0, type: "category", data: ["A", "B"], name: "广告版本", nameLocation: "middle", nameGap: 30, nameTextStyle: { fontSize: 14 } },
|
|
|
|
|
|
{ gridIndex: 1, type: "category", data: ["A", "B"], name: "广告版本", nameLocation: "middle", nameGap: 30, nameTextStyle: { fontSize: 14 } },
|
|
|
|
|
|
{ gridIndex: 2, type: "category", data: ["A", "B"], name: "广告版本", nameLocation: "middle", nameGap: 30, nameTextStyle: { fontSize: 14 } },
|
|
|
|
|
|
{ gridIndex: 3, type: "category", data: ["A", "B"], name: "广告版本", nameLocation: "middle", nameGap: 30, nameTextStyle: { fontSize: 14 } },
|
|
|
|
|
|
],
|
|
|
|
|
|
yAxis: [
|
|
|
|
|
|
{ gridIndex: 0, type: "value", name: "点击量", nameLocation: "middle", nameGap: 50, nameTextStyle: { fontSize: 14 } },
|
|
|
|
|
|
{ gridIndex: 1, type: "value", name: "曝光量", nameLocation: "middle", nameGap: 50, nameTextStyle: { fontSize: 14 } },
|
|
|
|
|
|
{ gridIndex: 2, type: "value", name: "转化量", nameLocation: "middle", nameGap: 50, nameTextStyle: { fontSize: 14 } },
|
|
|
|
|
|
{ gridIndex: 3, type: "value", name: "订单金额", nameLocation: "middle", nameGap: 50, nameTextStyle: { fontSize: 14 } },
|
|
|
|
|
|
],
|
|
|
|
|
|
series: [
|
|
|
|
|
|
// Clicks boxplot
|
|
|
|
|
|
{
|
|
|
|
|
|
name: "点击量",
|
|
|
|
|
|
type: "boxplot",
|
|
|
|
|
|
xAxisIndex: 0,
|
|
|
|
|
|
yAxisIndex: 0,
|
|
|
|
|
|
data: boxData.clicks,
|
|
|
|
|
|
itemStyle: {
|
|
|
|
|
|
borderColor: CHART_COLOR,
|
|
|
|
|
|
color: CHART_COLOR,
|
|
|
|
|
|
},
|
|
|
|
|
|
boxWidth: 150, // Increased box width
|
|
|
|
|
|
tooltip: {
|
|
|
|
|
|
formatter: (param) => [`${param.name}:`, `Upper: ${param.data[4].toFixed(2)}`, `Q3: ${param.data[3].toFixed(2)}`, `Median: ${param.data[2].toFixed(2)}`, `Q1: ${param.data[1].toFixed(2)}`, `Lower: ${param.data[0].toFixed(2)}`].join("<br/>"),
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
// Clicks outliers
|
|
|
|
|
|
{
|
|
|
|
|
|
name: "outlier",
|
|
|
|
|
|
type: "scatter",
|
|
|
|
|
|
xAxisIndex: 0,
|
|
|
|
|
|
yAxisIndex: 0,
|
|
|
|
|
|
data: outlierData.clicks.map((outliers, i) => outliers.map((val) => [i, val])),
|
|
|
|
|
|
itemStyle: {
|
|
|
|
|
|
color: "#fff",
|
|
|
|
|
|
borderColor: "#000",
|
|
|
|
|
|
borderWidth: 2,
|
|
|
|
|
|
},
|
|
|
|
|
|
symbolSize: 12, // Increased outlier size
|
|
|
|
|
|
},
|
|
|
|
|
|
// Exposure boxplot
|
|
|
|
|
|
{
|
|
|
|
|
|
name: "曝光量",
|
|
|
|
|
|
type: "boxplot",
|
|
|
|
|
|
xAxisIndex: 1,
|
|
|
|
|
|
yAxisIndex: 1,
|
|
|
|
|
|
data: boxData.exposure,
|
|
|
|
|
|
itemStyle: {
|
|
|
|
|
|
borderColor: CHART_COLOR,
|
|
|
|
|
|
color: CHART_COLOR,
|
|
|
|
|
|
},
|
|
|
|
|
|
boxWidth: 150, // Increased box width
|
|
|
|
|
|
tooltip: {
|
|
|
|
|
|
formatter: (param) => [`${param.name}:`, `Upper: ${param.data[4].toFixed(2)}`, `Q3: ${param.data[3].toFixed(2)}`, `Median: ${param.data[2].toFixed(2)}`, `Q1: ${param.data[1].toFixed(2)}`, `Lower: ${param.data[0].toFixed(2)}`].join("<br/>"),
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
// Exposure outliers
|
|
|
|
|
|
{
|
|
|
|
|
|
name: "outlier",
|
|
|
|
|
|
type: "scatter",
|
|
|
|
|
|
xAxisIndex: 1,
|
|
|
|
|
|
yAxisIndex: 1,
|
|
|
|
|
|
data: outlierData.exposure.map((outliers, i) => outliers.map((val) => [i, val])),
|
|
|
|
|
|
itemStyle: {
|
|
|
|
|
|
color: "#fff",
|
|
|
|
|
|
borderColor: "#000",
|
|
|
|
|
|
borderWidth: 2,
|
|
|
|
|
|
},
|
|
|
|
|
|
symbolSize: 12, // Increased outlier size
|
|
|
|
|
|
},
|
|
|
|
|
|
// Conversions boxplot
|
|
|
|
|
|
{
|
|
|
|
|
|
name: "转化量",
|
|
|
|
|
|
type: "boxplot",
|
|
|
|
|
|
xAxisIndex: 2,
|
|
|
|
|
|
yAxisIndex: 2,
|
|
|
|
|
|
data: boxData.conversions,
|
|
|
|
|
|
itemStyle: {
|
|
|
|
|
|
borderColor: CHART_COLOR,
|
|
|
|
|
|
color: CHART_COLOR,
|
|
|
|
|
|
},
|
|
|
|
|
|
boxWidth: 150, // Increased box width
|
|
|
|
|
|
|
|
|
|
|
|
tooltip: {
|
|
|
|
|
|
formatter: (param) => [`${param.name}:`, `Upper: ${param.data[4].toFixed(2)}`, `Q3: ${param.data[3].toFixed(2)}`, `Median: ${param.data[2].toFixed(2)}`, `Q1: ${param.data[1].toFixed(2)}`, `Lower: ${param.data[0].toFixed(2)}`].join("<br/>"),
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
// Conversions outliers
|
|
|
|
|
|
{
|
|
|
|
|
|
name: "outlier",
|
|
|
|
|
|
type: "scatter",
|
|
|
|
|
|
xAxisIndex: 2,
|
|
|
|
|
|
yAxisIndex: 2,
|
|
|
|
|
|
data: outlierData.conversions.map((outliers, i) => outliers.map((val) => [i, val])),
|
|
|
|
|
|
itemStyle: {
|
|
|
|
|
|
color: "#fff",
|
|
|
|
|
|
borderColor: "#000",
|
|
|
|
|
|
borderWidth: 2,
|
|
|
|
|
|
},
|
|
|
|
|
|
symbolSize: 12, // Increased outlier size
|
|
|
|
|
|
},
|
|
|
|
|
|
// Order Amount boxplot
|
|
|
|
|
|
{
|
|
|
|
|
|
name: "订单金额",
|
|
|
|
|
|
type: "boxplot",
|
|
|
|
|
|
xAxisIndex: 3,
|
|
|
|
|
|
yAxisIndex: 3,
|
|
|
|
|
|
data: boxData.orderAmount,
|
|
|
|
|
|
itemStyle: {
|
|
|
|
|
|
borderColor: CHART_COLOR,
|
|
|
|
|
|
color: CHART_COLOR,
|
|
|
|
|
|
},
|
|
|
|
|
|
boxWidth: 150, // Increased box width
|
|
|
|
|
|
tooltip: {
|
|
|
|
|
|
formatter: (param) => [`${param.name}:`, `Upper: ${param.data[4].toFixed(2)}`, `Q3: ${param.data[3].toFixed(2)}`, `Median: ${param.data[2].toFixed(2)}`, `Q1: ${param.data[1].toFixed(2)}`, `Lower: ${param.data[0].toFixed(2)}`].join("<br/>"),
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
// Order Amount outliers
|
|
|
|
|
|
{
|
|
|
|
|
|
name: "outlier",
|
|
|
|
|
|
type: "scatter",
|
|
|
|
|
|
xAxisIndex: 3,
|
|
|
|
|
|
yAxisIndex: 3,
|
|
|
|
|
|
data: outlierData.orderAmount.map((outliers, i) => outliers.map((val) => [i, val])),
|
|
|
|
|
|
itemStyle: {
|
|
|
|
|
|
color: "#fff",
|
|
|
|
|
|
borderColor: "#000",
|
|
|
|
|
|
borderWidth: 2,
|
|
|
|
|
|
},
|
|
|
|
|
|
symbolSize: 12, // Increased outlier size
|
|
|
|
|
|
},
|
|
|
|
|
|
],
|
|
|
|
|
|
};
|
|
|
|
|
|
chart.setOption(option);
|
|
|
|
|
|
// Handle window resize
|
|
|
|
|
|
const resizeHandler = () => chart.resize();
|
|
|
|
|
|
window.addEventListener("resize", resizeHandler);
|
|
|
|
|
|
// Cleanup
|
|
|
|
|
|
onUnmounted(() => {
|
|
|
|
|
|
window.removeEventListener("resize", resizeHandler);
|
|
|
|
|
|
chart.dispose();
|
|
|
|
|
|
});
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
ElMessage.error("图表渲染失败,请稍后重试");
|
|
|
|
|
|
console.error("Chart initialization error:", error);
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
chartLoading.value = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
// Helper Functions
|
|
|
|
|
|
function prepareBoxPlotData(data) {
|
|
|
|
|
|
const sorted = data.slice().sort((a, b) => a - b);
|
|
|
|
|
|
const len = sorted.length;
|
|
|
|
|
|
const q1 = sorted[Math.floor(len / 4)];
|
|
|
|
|
|
const median = sorted[Math.floor(len / 2)];
|
|
|
|
|
|
const q3 = sorted[Math.floor((len * 3) / 4)];
|
|
|
|
|
|
const iqr = q3 - q1;
|
|
|
|
|
|
const lowerBound = q1 - 1.5 * iqr;
|
|
|
|
|
|
const upperBound = q3 + 1.5 * iqr;
|
|
|
|
|
|
const lower = Math.max(sorted[0], lowerBound);
|
|
|
|
|
|
const upper = Math.min(sorted[len - 1], upperBound);
|
|
|
|
|
|
return [lower, q1, median, q3, upper];
|
|
|
|
|
|
}
|
|
|
|
|
|
function calcOutliers(data) {
|
|
|
|
|
|
const sorted = data.slice().sort((a, b) => a - b);
|
|
|
|
|
|
const len = sorted.length;
|
|
|
|
|
|
const q1 = sorted[Math.floor(len / 4)];
|
|
|
|
|
|
const q3 = sorted[Math.floor((len * 3) / 4)];
|
|
|
|
|
|
const iqr = q3 - q1;
|
|
|
|
|
|
const lowerBound = q1 - 1.5 * iqr;
|
|
|
|
|
|
const upperBound = q3 + 1.5 * iqr;
|
|
|
|
|
|
return data.filter((val) => val < lowerBound || val > upperBound);
|
|
|
|
|
|
}
|
|
|
|
|
|
function performTTest(a, b) {
|
|
|
|
|
|
const result = jStat.ttest(a, b, 2); // Two-sample t-test
|
|
|
|
|
|
return {
|
|
|
|
|
|
statistic: result.tstat,
|
|
|
|
|
|
pValue: result.p,
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
// Reset Task
|
|
|
|
|
|
function resetTask() {
|
|
|
|
|
|
analysisAPI.productReleaseReTrain({ userId: userStore.userInfo.userId, module: "产品推广测试" }).then((res) => {
|
|
|
|
|
|
proxy.$modal.msgSuccess("重新实训成功");
|
|
|
|
|
|
rawData1.value = [
|
|
|
|
|
|
{ version: "A (1月)", clicks: "", exposure: "", conversions: "", orderAmount: 2059 },
|
|
|
|
|
|
{ version: "A (2月)", clicks: "", exposure: "", conversions: "", orderAmount: 2515 },
|
|
|
|
|
|
{ version: "A (3月)", clicks: "", exposure: "", conversions: "", orderAmount: 2207 },
|
|
|
|
|
|
{ version: "A (4月)", clicks: "", exposure: "", conversions: "", orderAmount: 2436 },
|
|
|
|
|
|
{ version: "A (5月)", clicks: "", exposure: "", conversions: "", orderAmount: 2853 },
|
|
|
|
|
|
{ version: "B (1月)", clicks: "", exposure: "", conversions: "", orderAmount: 3079 },
|
|
|
|
|
|
{ version: "B (2月)", clicks: "", exposure: "", conversions: "", orderAmount: 3507 },
|
|
|
|
|
|
{ version: "B (3月)", clicks: "", exposure: "", conversions: "", orderAmount: 3215 },
|
|
|
|
|
|
{ version: "B (4月)", clicks: "", exposure: "", conversions: "", orderAmount: 3831 },
|
|
|
|
|
|
{ version: "B (5月)", clicks: "", exposure: "", conversions: "", orderAmount: 4023 },
|
|
|
|
|
|
];
|
|
|
|
|
|
testResults.value = [
|
|
|
|
|
|
{ testName: "点击量", statistic: "", pValue: "" },
|
|
|
|
|
|
{ testName: "曝光量", statistic: "", pValue: "" },
|
|
|
|
|
|
{ testName: "转化量", statistic: "", pValue: "" },
|
|
|
|
|
|
{ testName: "订单金额", statistic: "", pValue: "" },
|
|
|
|
|
|
];
|
|
|
|
|
|
analysisText.value = "";
|
|
|
|
|
|
// 图表重置
|
|
|
|
|
|
chartLoading.value = true;
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
chartLoading.value = false;
|
|
|
|
|
|
initializeChart();
|
|
|
|
|
|
}, 1000);
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
function submitTask() {
|
|
|
|
|
|
if (rawData1.value.some((item) => !item.clicks || !item.exposure || !item.conversions)) {
|
|
|
|
|
|
return proxy.$modal.msgWarning("请先指标数据计算填写!");
|
|
|
|
|
|
}
|
|
|
|
|
|
if (testResults.value.some((item) => !item.statistic || !item.pValue)) {
|
|
|
|
|
|
return proxy.$modal.msgWarning("请先完成统计显著性检验!");
|
|
|
|
|
|
}
|
|
|
|
|
|
let params = {
|
|
|
|
|
|
userId: userStore.userInfo.userId,
|
|
|
|
|
|
clickRate: rawData1.value.map((item) => item.clicks).join(","),
|
|
|
|
|
|
conversionRate: rawData1.value.map((item) => item.exposure).join(","),
|
|
|
|
|
|
roiData: rawData1.value.map((item) => item.conversions).join(","),
|
|
|
|
|
|
pValue: testResults.value.map((item) => item.pValue).join(","),
|
|
|
|
|
|
orderAmount: testResults.value.map((item) => item.statistic).join(","),
|
|
|
|
|
|
context: analysisText.value,
|
|
|
|
|
|
};
|
|
|
|
|
|
analysisAPI.productPromotionTestSubmit(params).then((res) => {});
|
|
|
|
|
|
}
|
|
|
|
|
|
function closeCode() {
|
|
|
|
|
|
runResultShow.value = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
// 回显数据
|
|
|
|
|
|
function loadTaskInfo() {
|
|
|
|
|
|
analysisAPI.productPromotionTestEcho({ userId: userStore.userInfo.userId }).then((res) => {
|
|
|
|
|
|
if (!res.data) return;
|
|
|
|
|
|
const clickRate = res.data.clickRate.split(",");
|
|
|
|
|
|
const conversionRate = res.data.conversionRate.split(",");
|
|
|
|
|
|
const roiData = res.data.roiData.split(",");
|
|
|
|
|
|
const pValue = res.data.pValue.split(",");
|
|
|
|
|
|
const orderAmount = res.data.orderAmount.split(",");
|
|
|
|
|
|
rawData1.value.forEach((item, index) => {
|
|
|
|
|
|
item.clicks = clickRate[index] || "";
|
|
|
|
|
|
item.exposure = conversionRate[index] || "";
|
|
|
|
|
|
item.conversions = roiData[index] || "";
|
|
|
|
|
|
});
|
|
|
|
|
|
// Update testResults with echoed data
|
|
|
|
|
|
testResults.value.forEach((item, index) => {
|
|
|
|
|
|
item.pValue = pValue[index];
|
|
|
|
|
|
item.statistic = orderAmount[index];
|
|
|
|
|
|
});
|
|
|
|
|
|
context.value = res.data.context;
|
|
|
|
|
|
chartLoading.value = false;
|
|
|
|
|
|
initializeChart();
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
const go=()=>{
|
|
|
|
|
|
window.open('https://jrdsj.sztzjy.com:8000/user/zy84cf311/lab')
|
|
|
|
|
|
}
|
|
|
|
|
|
// Lifecycle
|
|
|
|
|
|
onMounted(() => {
|
|
|
|
|
|
rawData.value = calculateMetrics(rawData.value);
|
|
|
|
|
|
const observer = new MutationObserver(() => {
|
|
|
|
|
|
if (chartContainer.value) {
|
|
|
|
|
|
observer.disconnect();
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
observer.observe(document.body, { childList: true, subtree: true });
|
|
|
|
|
|
loadTaskInfo();
|
|
|
|
|
|
});
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<style lang="scss" scoped>
|
|
|
|
|
|
.promotion {
|
|
|
|
|
|
background: #ffffff;
|
|
|
|
|
|
.intro,
|
|
|
|
|
|
.case-study,
|
|
|
|
|
|
.analysis,
|
|
|
|
|
|
.statistical-test,
|
|
|
|
|
|
.results {
|
|
|
|
|
|
margin-bottom: 30px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
h3 {
|
|
|
|
|
|
font-size: 18px;
|
|
|
|
|
|
color: var(--text-color);
|
|
|
|
|
|
margin-bottom: 15px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.case-backtitle {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
margin-bottom: 15px;
|
|
|
|
|
|
|
|
|
|
|
|
img {
|
|
|
|
|
|
width: 24px;
|
|
|
|
|
|
height: 24px;
|
|
|
|
|
|
margin-right: 8px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
span {
|
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
|
color: var(--text-color);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.data-table {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
max-width: 1000px;
|
|
|
|
|
|
margin: 20px 0;
|
|
|
|
|
|
display: block;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.formulas {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
|
gap: 20px;
|
|
|
|
|
|
margin-bottom: 20px;
|
|
|
|
|
|
|
|
|
|
|
|
span {
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
color: var(--text-color);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.chart-container {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
max-width: 1500px;
|
|
|
|
|
|
height: 600px; /* Increased height for better visibility */
|
|
|
|
|
|
margin: 20px 0;
|
|
|
|
|
|
display: block;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.task-footer {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: 10px;
|
|
|
|
|
|
justify-content: flex-end;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@media (max-width: 768px) {
|
|
|
|
|
|
padding: 10px;
|
|
|
|
|
|
|
|
|
|
|
|
.data-table,
|
|
|
|
|
|
.chart-container {
|
|
|
|
|
|
max-width: 100%;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.chart-container {
|
|
|
|
|
|
height: 400px; /* Adjusted for smaller screens */
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.formulas {
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: 10px;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
</style>
|