AB测试可视化看板设计与实现 原创

进修的泡芙
发布于 2025-6-16 19:25
浏览
0收藏

AB测试可视化看板设计与实现

一、项目概述

基于HarmonyOS 5和AGC(华为应用市场服务)Remote Config构建的AB测试可视化看板系统,用于实时展示不同实验组的转化率对比数据。该系统借鉴《鸿蒙跨端U同步》中游戏场景的多设备数据同步机制,实现实验配置的实时更新和用户行为数据的跨设备采集,为产品决策提供数据支持。

二、核心架构设计

±--------------------+
AGC Remote Config
(实验配置中心)

±---------±---------+
±---------v----------+ ±--------------------+

数据采集SDK <—> 用户行为分析服务
(Data Collector) (Analytics)

±---------±---------+ ±--------------------+
±---------v----------+

可视化看板
(Dashboard)

±--------------------+

三、AGC Remote Config集成

// Remote Config管理类
public class RemoteConfigManager {
private static final String TAG = “RemoteConfig”;
private AGConnectRemoteConfig config;

public RemoteConfigManager(Context context) {
    AGConnectInstance.initialize(context);
    this.config = AGConnectRemoteConfig.getInstance();
    
    // 设置默认参数
    Map<String, Object> defaults = new HashMap<>();
    defaults.put("ab_test_group", "control");
    defaults.put("new_feature_enabled", false);
    config.applyDefault(defaults);

// 获取用户分组

public String getABTestGroup() {
    return config.getValueAsString("ab_test_group");

// 检查新功能是否启用

public boolean isNewFeatureEnabled() {
    return config.getValueAsBoolean("new_feature_enabled");

// 获取配置参数

public String getConfigValue(String key) {
    return config.getValueAsString(key);

// 刷新远程配置

public void fetchConfig(ConfigCallback callback) {
    config.fetch().addOnSuccessListener(configValues -> {
        config.apply(configValues);
        callback.onSuccess();
    }).addOnFailureListener(e -> {
        callback.onError(e);
    });

public interface ConfigCallback {

    void onSuccess();
    void onError(Exception e);

}

四、数据采集实现

// 用户行为数据采集器
public class AnalyticsCollector {
private static final String TAG = “Analytics”;
private HiAnalyticsInstance analytics;
private RemoteConfigManager configManager;

public AnalyticsCollector(Context context) {
    this.analytics = HiAnalytics.getInstance(context);
    this.configManager = new RemoteConfigManager(context);

// 记录用户事件

public void logEvent(String eventName, Bundle params) {
    // 添加AB测试分组信息
    params.putString("ab_test_group", configManager.getABTestGroup());
    
    analytics.onEvent(eventName, params);

// 记录转化事件

public void logConversion(String conversionPoint) {
    Bundle params = new Bundle();
    params.putString("conversion_point", conversionPoint);
    params.putString("ab_test_group", configManager.getABTestGroup());
    
    analytics.onEvent("conversion", params);

// 记录页面访问

public void logPageView(String pageName) {
    Bundle params = new Bundle();
    params.putString("page_name", pageName);
    params.putString("ab_test_group", configManager.getABTestGroup());
    
    analytics.onEvent("page_view", params);

}

五、可视化看板实现
数据获取服务

// AB测试数据服务
public class ABTestDataService {
private static final String TAG = “ABTestData”;
private AGConnectCloudDB cloudDB;

public ABTestDataService(Context context) {
    CloudDBZoneConfig config = new CloudDBZoneConfig("abtest_data",
        CloudDBZoneConfig.CloudDBZoneSyncProperty.CLOUDDBZONE_CLOUD_CACHE,
        CloudDBZoneConfig.CloudDBZoneAccessProperty.CLOUDDBZONE_PUBLIC);
    
    AGConnectCloudDB.initialize(context);
    cloudDB = AGConnectCloudDB.getInstance();
    cloudDB.createCloudDBZone(config);

// 获取实验组转化数据

public void getConversionData(String experimentId, DataCallback callback) {
    CloudDBZoneQuery<ABTestResult> query = CloudDBZoneQuery.where(ABTestResult.class)
        .equalTo("experimentId", experimentId);
        
    cloudDB.executeQuery(query, QueryPolicy.POLICY_QUERY_FROM_CLOUD_ONLY)
        .addOnSuccessListener(snapshot -> {
            List<ABTestResult> results = new ArrayList<>();
            while (snapshot.hasNext()) {
                results.add(snapshot.next());

callback.onDataLoaded(results);

        })
        .addOnFailureListener(e -> {
            callback.onError(e);
        });

// 获取多设备实验数据

public void getMultiDeviceData(String experimentId, MultiDeviceCallback callback) {
    CloudDBZoneQuery<DeviceExperimentData> query = CloudDBZoneQuery.where(DeviceExperimentData.class)
        .equalTo("experimentId", experimentId);
        
    cloudDB.executeQuery(query, QueryPolicy.POLICY_QUERY_FROM_CLOUD_ONLY)
        .addOnSuccessListener(snapshot -> {
            Map<String, List<DeviceExperimentData>> deviceDataMap = new HashMap<>();
            while (snapshot.hasNext()) {
                DeviceExperimentData data = snapshot.next();
                if (!deviceDataMap.containsKey(data.getDeviceType())) {
                    deviceDataMap.put(data.getDeviceType(), new ArrayList<>());

deviceDataMap.get(data.getDeviceType()).add(data);

callback.onDataLoaded(deviceDataMap);

        })
        .addOnFailureListener(e -> {
            callback.onError(e);
        });

public interface DataCallback {

    void onDataLoaded(List<ABTestResult> results);
    void onError(Exception e);

public interface MultiDeviceCallback {

    void onDataLoaded(Map<String, List<DeviceExperimentData>> deviceData);
    void onError(Exception e);

}

数据可视化组件

// 转化率对比图表
public class ConversionChart extends Component {
private Paint groupAPaint;
private Paint groupBPaint;
private Paint axisPaint;
private Paint textPaint;

private List<ABTestResult> results;
private float maxConversionRate = 0;

public ConversionChart(Context context) {
    super(context);
    
    groupAPaint = new Paint();
    groupAPaint.setColor(0xFF4285F4); // Google Blue
    groupAPaint.setStyle(Paint.Style.FILL);
    
    groupBPaint = new Paint();
    groupBPaint.setColor(0xFFEA4335); // Google Red
    groupBPaint.setStyle(Paint.Style.FILL);
    
    axisPaint = new Paint();
    axisPaint.setColor(Color.BLACK);
    axisPaint.setStrokeWidth(2);
    
    textPaint = new Paint();
    textPaint.setColor(Color.BLACK);
    textPaint.setTextSize(24);

public void setData(List<ABTestResult> results) {

    this.results = results;
    
    // 计算最大转化率用于缩放
    for (ABTestResult result : results) {
        if (result.getConversionRate() > maxConversionRate) {
            maxConversionRate = result.getConversionRate();

}

    invalidate();

@Override

public void onDraw(Component component, Canvas canvas) {
    super.onDraw(component, canvas);
    
    if (results == null || results.isEmpty()) {
        return;

int width = getWidth();

    int height = getHeight();
    int padding = 40;
    
    // 绘制坐标轴
    canvas.drawLine(padding, height - padding, width - padding, height - padding, axisPaint); // X轴
    canvas.drawLine(padding, padding, padding, height - padding, axisPaint); // Y轴
    
    // 绘制刻度
    for (int i = 0; i <= 10; i++) {
        float y = height - padding - (height - 2  padding)  i / 10;
        canvas.drawLine(padding - 5, y, padding, y, axisPaint);
        canvas.drawText(String.format("%d%%", i * 10), 10, y + 10, textPaint);

// 计算柱状图参数

    int barCount = results.size();
    float barWidth = (width - 2  padding) / (barCount  3); // 每组两个柱状图加间距
    float startX = padding + barWidth;
    
    // 绘制柱状图
    for (int i = 0; i < barCount; i++) {
        ABTestResult result = results.get(i);
        
        // 实验组A
        float barHeightA = (height - 2  padding)  result.getGroupAConversion() / maxConversionRate;
        canvas.drawRect(
            startX, 
            height - padding - barHeightA,
            startX + barWidth,
            height - padding,
            groupAPaint
        );
        
        // 实验组B
        float barHeightB = (height - 2  padding)  result.getGroupBConversion() / maxConversionRate;
        canvas.drawRect(
            startX + barWidth * 1.5f,
            height - padding - barHeightB,
            startX + barWidth * 2.5f,
            height - padding,
            groupBPaint
        );
        
        // 绘制标签
        canvas.drawText(
            result.getDate().substring(5), // 显示月日
            startX + barWidth * 1.25f,
            height - padding / 2,
            textPaint
        );
        
        startX += barWidth * 3;

// 绘制图例

    canvas.drawText("对照组", width - 150, 50, textPaint);
    canvas.drawRect(width - 180, 35, width - 160, 55, groupAPaint);
    
    canvas.drawText("实验组", width - 150, 80, textPaint);
    canvas.drawRect(width - 180, 65, width - 160, 85, groupBPaint);

}

多设备数据展示

// 多设备数据对比组件
public class DeviceComparisonChart extends Component {
private Paint[] devicePaints;
private Paint axisPaint;
private Paint textPaint;

private Map<String, List<DeviceExperimentData>> deviceData;

public DeviceComparisonChart(Context context) {
    super(context);
    
    // 初始化不同设备的颜色
    devicePaints = new Paint[5];
    int[] colors = {0xFF4285F4, 0xFFEA4335, 0xFFFBBC05, 0xFF34A853, 0xFF673AB7};
    
    for (int i = 0; i < devicePaints.length; i++) {
        devicePaints[i] = new Paint();
        devicePaints[i].setColor(colors[i]);
        devicePaints[i].setStyle(Paint.Style.FILL);

axisPaint = new Paint();

    axisPaint.setColor(Color.BLACK);
    axisPaint.setStrokeWidth(2);
    
    textPaint = new Paint();
    textPaint.setColor(Color.BLACK);
    textPaint.setTextSize(24);

public void setDeviceData(Map<String, List<DeviceExperimentData>> deviceData) {

    this.deviceData = deviceData;
    invalidate();

@Override

public void onDraw(Component component, Canvas canvas) {
    super.onDraw(component, canvas);
    
    if (deviceData == null || deviceData.isEmpty()) {
        return;

int width = getWidth();

    int height = getHeight();
    int padding = 40;
    
    // 绘制坐标轴
    canvas.drawLine(padding, height - padding, width - padding, height - padding, axisPaint); // X轴
    canvas.drawLine(padding, padding, padding, height - padding, axisPaint); // Y轴
    
    // 绘制刻度
    for (int i = 0; i <= 10; i++) {
        float y = height - padding - (height - 2  padding)  i / 10;
        canvas.drawLine(padding - 5, y, padding, y, axisPaint);
        canvas.drawText(String.format("%d%%", i * 10), 10, y + 10, textPaint);

// 准备设备数据

    List<String> deviceTypes = new ArrayList<>(deviceData.keySet());
    int deviceCount = Math.min(deviceTypes.size(), devicePaints.length);
    
    // 计算柱状图参数
    float barWidth = (width - 2  padding) / (deviceCount  2);
    float startX = padding + barWidth;
    
    // 绘制每个设备的转化率
    for (int i = 0; i < deviceCount; i++) {
        String deviceType = deviceTypes.get(i);
        List<DeviceExperimentData> dataList = deviceData.get(deviceType);
        
        // 计算该设备类型的平均转化率
        float totalConversion = 0;
        for (DeviceExperimentData data : dataList) {
            totalConversion += data.getConversionRate();

float avgConversion = totalConversion / dataList.size();

        // 绘制柱状图
        float barHeight = (height - 2  padding)  avgConversion / 100;
        canvas.drawRect(
            startX,
            height - padding - barHeight,
            startX + barWidth,
            height - padding,
            devicePaints[i]
        );
        
        // 绘制设备标签
        String shortName = deviceType.length() > 8 ? 
            deviceType.substring(0, 5) + "..." : deviceType;
        canvas.drawText(
            shortName,
            startX,
            height - padding / 2,
            textPaint
        );
        
        startX += barWidth * 1.5f;

// 绘制图例

    int legendY = 50;
    for (int i = 0; i < deviceCount; i++) {
        canvas.drawRect(width - 180, legendY - 15, width - 160, legendY + 5, devicePaints[i]);
        canvas.drawText(deviceTypes.get(i), width - 150, legendY, textPaint);
        legendY += 30;

}

六、完整看板实现

// AB测试看板Ability
public class ABTestDashboardAbilitySlice extends AbilitySlice {
private ConversionChart conversionChart;
private DeviceComparisonChart deviceChart;
private Text statsTextView;

private ABTestDataService dataService;
private RemoteConfigManager configManager;

@Override
public void onStart(Intent intent) {
    super.onStart(intent);
    setUIContent(ResourceTable.Layout_abtest_dashboard);
    
    // 初始化UI组件
    conversionChart = (ConversionChart) findComponentById(ResourceTable.Id_conversion_chart);
    deviceChart = (DeviceComparisonChart) findComponentById(ResourceTable.Id_device_chart);
    statsTextView = (Text) findComponentById(ResourceTable.Id_stats_text);
    
    // 初始化服务
    dataService = new ABTestDataService(this);
    configManager = new RemoteConfigManager(this);
    
    // 加载当前实验配置
    loadCurrentExperiment();

private void loadCurrentExperiment() {

    String currentExperiment = configManager.getConfigValue("current_experiment");
    if (currentExperiment != null && !currentExperiment.isEmpty()) {
        refreshData(currentExperiment);

}

private void refreshData(String experimentId) {
    // 加载转化率数据
    dataService.getConversionData(experimentId, new ABTestDataService.DataCallback() {
        @Override
        public void onDataLoaded(List<ABTestResult> results) {
            getUITaskDispatcher().asyncDispatch(() -> {
                conversionChart.setData(results);
                updateStatsText(results);
            });

@Override

        public void onError(Exception e) {
            showError("加载转化数据失败: " + e.getMessage());

});

    // 加载多设备数据
    dataService.getMultiDeviceData(experimentId, new ABTestDataService.MultiDeviceCallback() {
        @Override
        public void onDataLoaded(Map<String, List<DeviceExperimentData>> deviceData) {
            getUITaskDispatcher().asyncDispatch(() -> {
                deviceChart.setDeviceData(deviceData);
            });

@Override

        public void onError(Exception e) {
            showError("加载设备数据失败: " + e.getMessage());

});

private void updateStatsText(List<ABTestResult> results) {

    if (results == null || results.isEmpty()) {
        return;

ABTestResult latest = results.get(results.size() - 1);

    double lift = (latest.getGroupBConversion() - latest.getGroupAConversion()) / 
                 latest.getGroupAConversion() * 100;
    
    String statsText = String.format(Locale.getDefault(),
        "最新数据:\n" +
        "对照组转化率: %.2f%%\n" +
        "实验组转化率: %.2f%%\n" +
        "提升效果: %.1f%%\n" +
        "统计显著性: %s",
        latest.getGroupAConversion(),
        latest.getGroupBConversion(),
        lift,
        latest.isSignificant() ? "显著" : "不显著"
    );
    
    statsTextView.setText(statsText);

private void showError(String message) {

    getUITaskDispatcher().asyncDispatch(() -> {
        new ToastDialog(this)
            .setText(message)
            .show();
    });

}

七、技术创新点
实时数据同步:基于AGC Remote Config实现实验配置实时更新

多维度分析:支持时间趋势和多设备类型对比

可视化丰富:提供直观的图表展示转化率差异

跨设备支持:借鉴鸿蒙跨端同步机制实现多设备数据采集

决策支持:自动计算提升效果和统计显著性

八、总结

本AB测试可视化看板系统基于HarmonyOS 5和AGC服务,实现了以下核心价值:
实验监控:实时跟踪不同实验组的表现差异

数据驱动:为产品决策提供可靠的量化依据

效率提升:快速验证新功能或改版效果

跨端一致:确保多设备用户的实验体验一致

系统借鉴了《鸿蒙跨端U同步》中的数据同步和展示机制,将游戏场景的实时同步需求应用于产品实验分析领域。未来可结合预测模型预估实验效果,并与功能发布系统集成,实现从实验到发布的自动化流程。

©著作权归作者所有,如需转载,请注明出处,否则将追究法律责任
收藏
回复
举报
回复
    相关推荐