PHP项目A/B测试与优化

wen PHP项目 3

PHP项目A/B测试与优化完整指南

A/B测试基础架构

基本测试引擎设计

<?php
class ABTestEngine {
    private $redis;
    private $experiments = [];
    public function __construct() {
        $this->redis = new Redis();
        $this->redis->connect('127.0.0.1', 6379);
        $this->loadExperiments();
    }
    // 加载实验配置
    private function loadExperiments() {
        $this->experiments = [
            'homepage_layout' => [
                'variants' => ['control' => 50, 'variant_a' => 25, 'variant_b' => 25],
                'tracking' => ['conversion', 'click', 'time_on_page']
            ],
            'pricing_page' => [
                'variants' => ['control' => 50, 'variant_a' => 50],
                'tracking' => ['signup', 'revenue']
            ]
        ];
    }
    // 为用户分配实验组
    public function assignVariant($userId, $experimentName) {
        $cacheKey = "ab_test:{$experimentName}:{$userId}";
        // 检查是否已有分配
        $variant = $this->redis->get($cacheKey);
        if ($variant) {
            return $variant;
        }
        // 新分配
        $experiment = $this->experiments[$experimentName];
        $variant = $this->weightedRandom($experiment['variants']);
        // 缓存分配结果(24小时过期)
        $this->redis->setex($cacheKey, 86400, $variant);
        return $variant;
    }
    // 加权随机分配
    private function weightedRandom($variants) {
        $totalWeight = array_sum($variants);
        $random = mt_rand(1, $totalWeight);
        $cumulative = 0;
        foreach ($variants as $variant => $weight) {
            $cumulative += $weight;
            if ($random <= $cumulative) {
                return $variant;
            }
        }
        return 'control';
    }
    // 记录事件
    public function trackEvent($userId, $experimentName, $event, $data = []) {
        $variant = $this->assignVariant($userId, $experimentName);
        $eventData = [
            'user_id' => $userId,
            'experiment' => $experimentName,
            'variant' => $variant,
            'event' => $event,
            'data' => json_encode($data),
            'timestamp' => time()
        ];
        // 异步记录到数据库或消息队列
        $this->logEventAsync($eventData);
    }
    private function logEventAsync($eventData) {
        // 使用消息队列(如RabbitMQ/Redis队列)
        $this->redis->lPush('ab_test_events', json_encode($eventData));
    }
}

中间件集成(Laravel示例)

<?php
namespace App\Http\Middleware;
use Closure;
use App\Services\ABTestEngine;
class ABTestMiddleware {
    protected $abTest;
    public function __construct(ABTestEngine $abTest) {
        $this->abTest = $abTest;
    }
    public function handle($request, Closure $next) {
        $userId = $request->user() ? $request->user()->id : session()->getId();
        // 将实验组信息添加到请求
        $request->attributes->add([
            'ab_test_variant' => $this->abTest->assignVariant($userId, 'homepage_layout'),
            'ab_test_user_id' => $userId
        ]);
        $response = $next($request);
        // 添加实验标识到响应头(用于调试)
        $response->headers->set('X-AB-Test', $request->attributes->get('ab_test_variant'));
        return $response;
    }
}

前端集成方案

Blade模板集成(Laravel)

{{-- resources/views/home.blade.php --}}
@php
    $variant = request()->attributes->get('ab_test_variant');
@endphp
@extends('layouts.app')
@section('content')
    <div class="container">
        @if($variant === 'variant_a')
            {{-- 新布局 A --}}
            <div class="new-layout-a">
                <h1>优化版首页</h1>
                <button class="cta-button cta-red" onclick="trackEvent('cta_click')">
                    立即体验
                </button>
            </div>
        @elseif($variant === 'variant_b')
            {{-- 新布局 B --}}
            <div class="new-layout-b">
                <h1>全新设计</h1>
                <button class="cta-button cta-blue" onclick="trackEvent('cta_click')">
                    免费试用
                </button>
            </div>
        @else
            {{-- 默认布局 --}}
            <div class="old-layout">
                <h1>欢迎回来</h1>
                <a href="/register" class="btn btn-primary">注册</a>
            </div>
        @endif
    </div>
    <script>
        function trackEvent(eventName) {
            fetch('/api/track', {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                    'X-CSRF-TOKEN': '{{ csrf_token() }}'
                },
                body: JSON.stringify({
                    experiment: 'homepage_layout',
                    event: eventName,
                    variant: '{{ $variant }}'
                })
            });
        }
    </script>
@endsection

JavaScript追踪器

// resources/js/ab-testing.js
class ABTestTracker {
    constructor(options = {}) {
        this.apiEndpoint = options.apiEndpoint || '/api/track';
        this.experimentName = document.querySelector('meta[name="ab-experiment"]')?.content;
        this.variant = document.querySelector('meta[name="ab-variant"]')?.content;
        this.sessionId = this.getSessionId();
        // 自动追踪页面浏览
        if (this.experimentName) {
            this.track('pageview');
        }
    }
    track(event, data = {}) {
        if (!this.experimentName) return;
        const payload = {
            experiment: this.experimentName,
            variant: this.variant,
            event: event,
            data: {
                ...data,
                url: window.location.href,
                user_agent: navigator.userAgent,
                timestamp: Date.now()
            }
        };
        // 使用Beacon API确保页面关闭时也能发送
        if (navigator.sendBeacon) {
            navigator.sendBeacon(this.apiEndpoint, JSON.stringify(payload));
        } else {
            // 降级使用fetch
            fetch(this.apiEndpoint, {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify(payload),
                keepalive: true
            });
        }
    }
    getSessionId() {
        let sessionId = sessionStorage.getItem('ab_session_id');
        if (!sessionId) {
            sessionId = 'sess_' + Math.random().toString(36).substr(2, 9);
            sessionStorage.setItem('ab_session_id', sessionId);
        }
        return sessionId;
    }
}
// 初始化
document.addEventListener('DOMContentLoaded', () => {
    window.ABTest = new ABTestTracker();
    // 追踪点击事件
    document.addEventListener('click', (e) => {
        const target = e.target.closest('[data-ab-track]');
        if (target) {
            window.ABTest.track('click', {
                element: target.getAttribute('data-ab-track'),
                text: target.innerText
            });
        }
    });
});

数据收集与存储

数据库表结构

-- 实验配置表
CREATE TABLE ab_experiments (
    id INT PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(100) UNIQUE NOT NULL,
    description TEXT,
    status ENUM('draft', 'running', 'paused', 'completed') DEFAULT 'draft',
    start_date DATETIME,
    end_date DATETIME,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 实验变体表
CREATE TABLE ab_variants (
    id INT PRIMARY KEY AUTO_INCREMENT,
    experiment_id INT NOT NULL,
    name VARCHAR(50) NOT NULL,
    weight INT DEFAULT 50,
    config JSON,
    FOREIGN KEY (experiment_id) REFERENCES ab_experiments(id)
);
-- 事件日志表(分区表)
CREATE TABLE ab_events (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    experiment_id INT NOT NULL,
    variant_id INT NOT NULL,
    user_id VARCHAR(100) NOT NULL,
    session_id VARCHAR(100),
    event_name VARCHAR(50) NOT NULL,
    event_data JSON,
    ip_address VARCHAR(45),
    user_agent TEXT,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    INDEX idx_experiment_event (experiment_id, event_name),
    INDEX idx_user_experiment (user_id, experiment_id),
    INDEX idx_created_at (created_at)
) PARTITION BY RANGE (TO_DAYS(created_at)) (
    PARTITION p202301 VALUES LESS THAN (TO_DAYS('2023-02-01')),
    PARTITION p202302 VALUES LESS THAN (TO_DAYS('2023-03-01')),
    -- 按月分区
    PARTITION p_future VALUES LESS THAN MAXVALUE
);
-- 分析汇总表
CREATE TABLE ab_analysis_results (
    id INT PRIMARY KEY AUTO_INCREMENT,
    experiment_id INT NOT NULL,
    variant_id INT NOT NULL,
    metric_name VARCHAR(50) NOT NULL,
    value DECIMAL(10,4),
    confidence_level DECIMAL(5,4),
    sample_size INT,
    calculated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    INDEX idx_experiment (experiment_id)
);

异步事件处理

<?php
// 消费者处理队列中的事件
class ABEventConsumer {
    private $redis;
    private $db;
    public function __construct() {
        $this->redis = new Redis();
        $this->redis->connect('127.0.0.1', 6379);
        $this->db = new PDO('mysql:host=localhost;dbname=test', 'user', 'pass');
    }
    public function processEvents() {
        while (true) {
            // 阻塞获取事件
            $event = $this->redis->brPop('ab_test_events', 5);
            if ($event) {
                $eventData = json_decode($event[1], true);
                $this->storeEvent($eventData);
            }
        }
    }
    private function storeEvent($eventData) {
        try {
            $stmt = $this->db->prepare(
                "INSERT INTO ab_events 
                (experiment_id, variant_id, user_id, event_name, event_data, created_at)
                VALUES (
                    (SELECT id FROM ab_experiments WHERE name = ?),
                    (SELECT id FROM ab_variants WHERE name = ? AND experiment_id = (SELECT id FROM ab_experiments WHERE name = ?)),
                    ?, ?, ?, NOW()
                )"
            );
            $stmt->execute([
                $eventData['experiment'],
                $eventData['variant'],
                $eventData['experiment'],
                $eventData['user_id'],
                $eventData['event'],
                json_encode($eventData['data'])
            ]);
        } catch (Exception $e) {
            // 记录错误到日志
            error_log("Failed to store AB event: " . $e->getMessage());
        }
    }
}

统计分析

核心分析引擎

<?php
class ABTestAnalyzer {
    private $db;
    public function __construct() {
        $this->db = new PDO('mysql:host=localhost;dbname=test', 'user', 'pass');
    }
    // 计算转化率
    public function calculateConversionRate($experimentId) {
        $sql = "
            SELECT 
                v.name as variant,
                COUNT(DISTINCT CASE WHEN e.event_name = 'conversion' THEN e.user_id END) as conversions,
                COUNT(DISTINCT e.user_id) as total_users,
                COUNT(DISTINCT CASE WHEN e.event_name = 'conversion' THEN e.user_id END) / 
                COUNT(DISTINCT e.user_id) * 100 as conversion_rate
            FROM ab_variants v
            LEFT JOIN ab_events e ON v.id = e.variant_id 
                AND v.experiment_id = e.experiment_id
            WHERE v.experiment_id = ?
            GROUP BY v.id, v.name
        ";
        $stmt = $this->db->prepare($sql);
        $stmt->execute([$experimentId]);
        return $stmt->fetchAll(PDO::FETCH_ASSOC);
    }
    // 计算统计显著性(Z检验)
    public function calculateSignificance($experimentId, $metric = 'conversion') {
        $results = $this->calculateConversionRate($experimentId);
        // 使用控制组作为基准
        $control = array_filter($results, function($r) { return $r['variant'] === 'control'; });
        $control = reset($control);
        $significances = [];
        foreach ($results as $variant) {
            if ($variant['variant'] === 'control') continue;
            $p1 = $control['conversion_rate'] / 100;
            $p2 = $variant['conversion_rate'] / 100;
            $n1 = $control['total_users'];
            $n2 = $variant['total_users'];
            // 计算Z分数
            $p = ($p1 * $n1 + $p2 * $n2) / ($n1 + $n2);
            $se = sqrt($p * (1 - $p) * (1/$n1 + 1/$n2));
            $z = ($p2 - $p1) / $se;
            // 计算p值(使用正态分布近似)
            $p_value = 2 * (1 - $this->normalCDF(abs($z)));
            $significances[$variant['variant']] = [
                'lift' => (($p2 - $p1) / $p1) * 100,
                'z_score' => $z,
                'p_value' => $p_value,
                'significant' => $p_value < 0.05
            ];
        }
        return $significances;
    }
    // 正态分布CDF(近似)
    private function normalCDF($x) {
        $a1 =  0.254829592;
        $a2 = -0.284496736;
        $a3 =  1.421413741;
        $a4 = -1.453152027;
        $a5 =  1.061405429;
        $p  =  0.3275911;
        $sign = 1;
        if ($x < 0) {
            $sign = -1;
        }
        $x = abs($x) / sqrt(2);
        $t = 1.0 / (1.0 + $p * $x);
        $y = 1.0 - ((((($a5 * $t + $a4) * $t) + $a3) * $t + $a2) * $t + $a1) * $t * exp(-$x * $x);
        return 0.5 * (1.0 + $sign * $y);
    }
    // 计算贝叶斯概率
    public function bayesianAnalysis($experimentId) {
        $results = $this->calculateConversionRate($experimentId);
        // 使用贝塔分布进行贝叶斯推断
        $variants = [];
        foreach ($results as $v) {
            $variants[$v['variant']] = [
                'alpha' => $v['conversions'] + 1,
                'beta' => $v['total_users'] - $v['conversions'] + 1
            ];
        }
        // 蒙特卡洛模拟
        $simulations = 100000;
        $wins = array_fill_keys(array_keys($variants), 0);
        for ($i = 0; $i < $simulations; $i++) {
            $samples = [];
            foreach ($variants as $name => $params) {
                // 使用Box-Muller变换生成贝塔分布样本
                $samples[$name] = $this->betaSample($params['alpha'], $params['beta']);
            }
            $maxVariant = array_keys($samples, max($samples))[0];
            $wins[$maxVariant]++;
        }
        // 计算每个变体是最优的概率
        $probabilities = [];
        foreach ($wins as $variant => $count) {
            $probabilities[$variant] = $count / $simulations;
        }
        return $probabilities;
    }
    private function betaSample($alpha, $beta) {
        $x = $this->gammaSample($alpha);
        $y = $this->gammaSample($beta);
        return $x / ($x + $y);
    }
    private function gammaSample($shape) {
        // Marsaglia & Tsang方法生成Gamma分布样本
        if ($shape < 1) {
            $u = mt_rand() / mt_getrandmax();
            return $this->gammaSample($shape + 1) * pow($u, 1 / $shape);
        }
        $d = $shape - 1/3;
        $c = 1 / sqrt(9 * $d);
        while (true) {
            $x = $this->normalSample();
            $v = 1 + $c * $x;
            if ($v <= 0) continue;
            $v = $v * $v * $v;
            $u = mt_rand() / mt_getrandmax();
            if ($u < 1 - 0.0331 * $x * $x * $x * $x) return $d * $v;
            if (log($u) < 0.5 * $x * $x + $d * (1 - $v + log($v))) return $d * $v;
        }
    }
    private function normalSample() {
        // Box-Muller变换
        $u1 = mt_rand() / mt_getrandmax();
        $u2 = mt_rand() / mt_getrandmax();
        return sqrt(-2 * log($u1)) * cos(2 * M_PI * $u2);
    }
}

管理员数据分析面板

<?php
// routes/web.php
Route::middleware(['auth', 'admin'])->group(function () {
    Route::get('/admin/ab-testing', 'ABTestController@dashboard');
    Route::get('/admin/ab-testing/{id}/results', 'ABTestController@results');
    Route::post('/admin/ab-testing/{id}/stop', 'ABTestController@stopExperiment');
});
// app/Http/Controllers/ABTestController.php
class ABTestController extends Controller {
    public function dashboard() {
        $experiments = ABExperiment::with('variants')->where('status', 'running')->get();
        $data = [];
        foreach ($experiments as $experiment) {
            $analyzer = new ABTestAnalyzer();
            $data[] = [
                'experiment' => $experiment,
                'conversion_rates' => $analyzer->calculateConversionRate($experiment->id),
                'significance' => $analyzer->calculateSignificance($experiment->id),
                'bayesian' => $analyzer->bayesianAnalysis($experiment->id)
            ];
        }
        return view('admin.ab-testing.dashboard', compact('data'));
    }
    public function results($id) {
        $experiment = ABExperiment::with('variants')->findOrFail($id);
        $analyzer = new ABTestAnalyzer();
        return view('admin.ab-testing.results', [
            'experiment' => $experiment,
            'conversion_rates' => $analyzer->calculateConversionRate($id),
            'significance' => $analyzer->calculateSignificance($id),
            'bayesian' => $analyzer->bayesianAnalysis($id)
        ]);
    }
}

性能优化

减少数据库查询

<?php
// 使用Redis缓存实验结果
class OptimizedABTestEngine {
    private $redis;
    public function getVariant($userId, $experimentName) {
        // 批量获取
        $variants = $this->redis->mget([
            "ab_test:{$experimentName}:{$userId}",
            "ab_test:{$experimentName}:config"
        ]);
        if ($variants[0]) {
            return $variants[0];
        }
        // 缓存实验配置
        if (!$variants[1]) {
            $config = $this->loadConfig($experimentName);
            $this->redis->setex("ab_test:{$experimentName}:config", 3600, json_encode($config));
        } else {
            $config = json_decode($variants[1], true);
        }
        // 分配并缓存
        $variant = $this->assignVariantFromConfig($userId, $config);
        $this->redis->setex("ab_test:{$experimentName}:{$userId}", 86400, $variant);
        return $variant;
    }
}

批量事件处理

<?php
// 批量写入事件
class BatchEventProcessor {
    private $buffer = [];
    private $bufferSize = 100;
    public function addEvent($event) {
        $this->buffer[] = $event;
        if (count($this->buffer) >= $this->bufferSize) {
            $this->flush();
        }
    }
    public function flush() {
        if (empty($this->buffer)) return;
        DB::transaction(function () {
            $chunks = array_chunk($this->buffer, 50);
            foreach ($chunks as $chunk) {
                $insertData = array_map(function($event) {
                    return [
                        'experiment_id' => $event['experiment_id'],
                        'variant_id' => $event['variant_id'],
                        'user_id' => $event['user_id'],
                        'event_name' => $event['event_name'],
                        'event_data' => json_encode($event['event_data'])
                    ];
                }, $chunk);
                ABEvent::insert($insertData);
            }
        });
        $this->buffer = [];
    }
    public function __destruct() {
        $this->flush();
    }
}

完整优化策略

多变量测试(MVT)

<?php
// 同时测试多个元素组合
class MVTEngine {
    private $factors = [
        'headline' => ['Welcome', 'Get Started', 'Join Now'],
        'cta_color' => ['red', 'blue', 'green'],
        'image' => ['hero1.jpg', 'hero2.jpg', 'hero3.jpg']
    ];
    public function getCombination($userId) {
        // 使用用户ID的哈希决定组合
        $hash = crc32($userId);
        $combinations = [];
        foreach ($this->factors as $factor => $options) {
            $index = abs($hash) % count($options);
            $combinations[$factor] = $options[$index];
            $hash = crc32($hash);
        }
        return $combinations;
    }
}

渐进式部署

<?php
// 渐进式流量分配
class ProgressiveDeployment {
    private $redis;
    public function rampUpTraffic($experimentName, $targetPercentage = 100, $days = 7) {
        $startTime = time();
        $this->redis->setex("ab_test:{$experimentName}:ramp_up", $days * 86400, json_encode([
            'start' => $startTime,
            'target' => $targetPercentage,
            'end' => $startTime + ($days * 86400)
        ]));
    }
    public function getCurrentTraffic($experimentName) {
        $rampUp = $this->redis->get("ab_test:{$experimentName}:ramp_up");
        if (!$rampUp) return 100; // 默认全量
        $config = json_decode($rampUp, true);
        $elapsed = time() - $config['start'];
        $total = $config['end'] - $config['start'];
        $progress = min(1, $elapsed / $total);
        return $config['target'] * $progress;
    }
}

实时监控与告警

<?php
class ABTestMonitor {
    public function checkForAnomalies($experimentId) {
        $hourlyData = $this->getHourlyData($experimentId);
        foreach ($hourlyData as $variant => $data) {
            // 检查是否有显著下降
            if (count($data) >= 2) {
                $recent = array_slice($data, -2);
                $change = ($recent[1] - $recent[0]) / $recent[0];
                if ($change < -0.1) { // 下降超过10%
                    $this->sendAlert($experimentId, $variant, $change);
                }
            }
        }
    }
    private function getHourlyData($experimentId) {
        return DB::table('ab_events')
            ->select(
                'variant_id',
                DB::raw("DATE_FORMAT(created_at, '%Y-%m-%d %H:00:00') as hour"),
                DB::raw('COUNT(DISTINCT user_id) as unique_users')
            )
            ->where('experiment_id', $experimentId)
            ->where('created_at', '>=', now()->subHours(24))
            ->groupBy('variant_id', 'hour')
            ->get()
            ->groupBy('variant_id')
            ->toArray();
    }
    private function sendAlert($experimentId, $variantId, $change) {
        // 发送邮件/Slack/Webhook通知
        $message = "异常告警:实验 #{$experimentId} 变体 #{$variantId} 在最近一小时内转化率下降 {$change}%";
        // 记录告警日志
        Log::warning($message, [
            'experiment_id' => $experimentId,
            'variant_id' => $variantId,
            'change_percentage' => $change
        ]);
    }
}

最佳实践建议

  1. 样本量计算:使用统计软件(如G*Power)提前计算所需样本量
  2. 多重比较校正:使用Bonferroni或FDR校正控制假阳性率
  3. 连续性监测:采用序贯分析避免过早停止实验
  4. 分层抽样:按用户特征(新/老用户、地域)分层确保组间均衡
  5. 版本控制:每个实验变体应该对应一个Git分支或配置文件

常用工具集成

# 安装PHP统计库
composer require stats/stats
# 集成Google Optimize
composer require google/optimize
# 使用数据分析库
composer require php-ml/php-ml

这个完整的A/B测试方案涵盖了从基础架构到高级统计分析的各个方面,可以根据项目需求选择性地实施各部分功能。

PHP项目A/B测试与优化

抱歉,评论功能暂时关闭!