如何用PHP项目实现多变量测试?

wen java案例 2

本文目录导读:

如何用PHP项目实现多变量测试?

  1. 多变量测试的基本概念
  2. 基础实现方案
  3. 数据库表结构
  4. 结果分析类
  5. 高级优化建议
  6. 最佳实践

我来详细说明如何用PHP项目实现多变量测试(Multivariate Testing,MVT)。

多变量测试的基本概念

多变量测试是同时测试多个页面元素的多个变体,以找到最佳组合的测试方法。

基础实现方案

1 定义测试变量和变体

<?php
// config/mvt-config.php
// 定义测试配置
$mvtConfig = [
    'homepage_hero' => [
        'enabled' => true,
        'name' => '首页横幅测试',
        'metrics' => ['click_rate', 'conversion_rate'],
        'variables' => [
            'headline' => [
                'variants' => [
                    'A' => 'Welcome to Our Platform',
                    'B' => 'Transform Your Business Today',
                    'C' => 'Get Started for Free'
                ],
                'weight' => [33, 33, 34] // 权重分配
            ],
            'button_color' => [
                'variants' => [
                    'A' => 'blue',
                    'B' => 'green', 
                    'C' => 'red'
                ],
                'weight' => [33, 33, 34]
            ],
            'cta_text' => [
                'variants' => [
                    'A' => 'Sign Up Now',
                    'B' => 'Get Started',
                    'C' => 'Try Free'
                ],
                'weight' => [33, 33, 34]
            ]
        ]
    ]
];

2 核心引擎实现

<?php
// src/MVTEngine.php
class MVTEngine {
    private $config;
    private $db;
    private $userId;
    private $variants = [];
    public function __construct($config, $db) {
        $this->config = $config;
        $this->db = $db;
        $this->userId = $this->getUserId();
    }
    /**
     * 获取用户分配的实验版本
     */
    public function assignVariants($testName) {
        if (!isset($this->config[$testName]) || !$this->config[$testName]['enabled']) {
            return $this->getDefaultVariants($testName);
        }
        // 检查用户是否已有分配
        $existing = $this->getExistingAssignment($testName);
        if ($existing) {
            return json_decode($existing['variants'], true);
        }
        // 为每个变量分配变体
        $test = $this->config[$testName];
        $assignedVariants = [];
        foreach ($test['variables'] as $varName => $variable) {
            $assignedVariants[$varName] = $this->getWeightedVariant(
                $variable['variants'],
                $variable['weight'],
                $testName . '_' . $varName
            );
        }
        // 保存分配
        $this->saveAssignment($testName, $assignedVariants);
        return $assignedVariants;
    }
    /**
     * 加权随机选择变体
     */
    private function getWeightedVariant($variants, $weights, $seed) {
        $variantKeys = array_keys($variants);
        $totalWeight = array_sum($weights);
        $random = mt_rand(1, $totalWeight);
        $cumulativeWeight = 0;
        foreach ($variantKeys as $index => $key) {
            $cumulativeWeight += $weights[$index];
            if ($random <= $cumulativeWeight) {
                return $key;
            }
        }
        return $variantKeys[0];
    }
    /**
     * 获取用户ID
     */
    private function getUserId() {
        // 使用session或cookie中的用户标识
        if (!isset($_SESSION['user_id'])) {
            $_SESSION['user_id'] = uniqid('user_', true);
        }
        return $_SESSION['user_id'];
    }
    /**
     * 保存测试分配
     */
    private function saveAssignment($testName, $variants) {
        $sql = "INSERT INTO mvt_assignments (user_id, test_name, variants, assigned_at) 
                VALUES (?, ?, ?, NOW())";
        $stmt = $this->db->prepare($sql);
        $stmt->execute([
            $this->userId,
            $testName,
            json_encode($variants)
        ]);
    }
    /**
     * 获取已存在的分配
     */
    private function getExistingAssignment($testName) {
        $sql = "SELECT variants FROM mvt_assignments 
                WHERE user_id = ? AND test_name = ? 
                ORDER BY assigned_at DESC LIMIT 1";
        $stmt = $this->db->prepare($sql);
        $stmt->execute([$this->userId, $testName]);
        return $stmt->fetch(PDO::FETCH_ASSOC);
    }
    /**
     * 获取默认变体
     */
    private function getDefaultVariants($testName) {
        $defaults = [];
        foreach ($this->config[$testName]['variables'] as $varName => $variable) {
            $defaults[$varName] = array_key_first($variable['variants']);
        }
        return $defaults;
    }
}

3 数据追踪类

<?php
// src/MVTTracker.php
class MVTTracker {
    private $db;
    public function __construct($db) {
        $this->db = $db;
    }
    /**
     * 记录事件
     */
    public function trackEvent($testName, $eventName, $userId, $variants) {
        $sql = "INSERT INTO mvt_events (test_name, event_name, user_id, variants, created_at) 
                VALUES (?, ?, ?, ?, NOW())";
        $stmt = $this->db->prepare($sql);
        $stmt->execute([
            $testName,
            $eventName,
            $userId,
            json_encode($variants)
        ]);
    }
    /**
     * 记录转化
     */
    public function trackConversion($testName, $userId, $variants, $conversionValue = null) {
        $sql = "INSERT INTO mvt_conversions (test_name, user_id, variants, conversion_value, created_at) 
                VALUES (?, ?, ?, ?, NOW())";
        $stmt = $this->db->prepare($sql);
        $stmt->execute([
            $testName,
            $userId,
            json_encode($variants),
            $conversionValue
        ]);
    }
}

4 前端集成示例

<?php
// templates/homepage.php
// 初始化MVT引擎
$mvtEngine = new MVTEngine($mvtConfig, $db);
$variants = $mvtEngine->assignVariants('homepage_hero');
// 获取当前页面的变体内容
$headline = $mvtConfig['homepage_hero']['variables']['headline']['variants'][$variants['headline']];
$buttonColor = $mvtConfig['homepage_hero']['variables']['button_color']['variants'][$variants['button_color']];
$ctaText = $mvtConfig['homepage_hero']['variables']['cta_text']['variants'][$variants['cta_text']];
?>
<!DOCTYPE html>
<html>
<head>MVT Test Page</title>
    <style>
        .cta-button {
            background-color: <?php echo htmlspecialchars($buttonColor); ?>;
            color: white;
        }
    </style>
</head>
<body>
    <div class="hero-section">
        <h1><?php echo htmlspecialchars($headline); ?></h1>
        <button class="cta-button" onclick="trackEvent('click')">
            <?php echo htmlspecialchars($ctaText); ?>
        </button>
    </div>
    <script>
        // 追踪数据
        const testData = {
            testName: 'homepage_hero',
            userId: '<?php echo $userId; ?>',
            variants: <?php echo json_encode($variants); ?>
        };
        // 追踪页面浏览
        fetch('/api/track.php', {
            method: 'POST',
            headers: {'Content-Type': 'application/json'},
            body: JSON.stringify({
                ...testData,
                event: 'page_view'
            })
        });
        // 追踪按钮点击
        function trackEvent(eventName) {
            fetch('/api/track.php', {
                method: 'POST',
                headers: {'Content-Type': 'application/json'},
                body: JSON.stringify({
                    ...testData,
                    event: eventName
                })
            }).then(() => {
                // 可以添加转化追踪逻辑
                if (eventName === 'signup_complete') {
                    trackConversion();
                }
            });
        }
        function trackConversion() {
            fetch('/api/conversion.php', {
                method: 'POST',
                headers: {'Content-Type': 'application/json'},
                body: JSON.stringify({
                    ...testData,
                    conversionValue: 100 // 转化价值
                })
            });
        }
    </script>
</body>
</html>

数据库表结构

-- 分配记录表
CREATE TABLE mvt_assignments (
    id INT AUTO_INCREMENT PRIMARY KEY,
    user_id VARCHAR(255) NOT NULL,
    test_name VARCHAR(100) NOT NULL,
    variants JSON NOT NULL,
    assigned_at DATETIME NOT NULL,
    INDEX idx_user_test (user_id, test_name)
);
-- 事件追踪表
CREATE TABLE mvt_events (
    id INT AUTO_INCREMENT PRIMARY KEY,
    test_name VARCHAR(100) NOT NULL,
    event_name VARCHAR(100) NOT NULL,
    user_id VARCHAR(255) NOT NULL,
    variants JSON NOT NULL,
    created_at DATETIME NOT NULL,
    INDEX idx_test_event (test_name, event_name, created_at)
);
-- 转化记录表
CREATE TABLE mvt_conversions (
    id INT AUTO_INCREMENT PRIMARY KEY,
    test_name VARCHAR(100) NOT NULL,
    user_id VARCHAR(255) NOT NULL,
    variants JSON NOT NULL,
    conversion_value DECIMAL(10,2) DEFAULT NULL,
    created_at DATETIME NOT NULL,
    INDEX idx_test_user (test_name, user_id)
);

结果分析类

<?php
// src/MVTResultsAnalyzer.php
class MVTResultsAnalyzer {
    private $db;
    public function __construct($db) {
        $this->db = $db;
    }
    /**
     * 获取测试结果统计
     */
    public function getTestResults($testName) {
        // 获取所有变体组合的统计数据
        $sql = "SELECT 
                    e.variants,
                    COUNT(DISTINCT e.user_id) as visitors,
                    COUNT(DISTINCT c.user_id) as conversions,
                    COUNT(c.user_id) / COUNT(DISTINCT e.user_id) * 100 as conversion_rate
                FROM mvt_events e
                LEFT JOIN mvt_conversions c 
                    ON e.test_name = c.test_name 
                    AND e.user_id = c.user_id
                WHERE e.test_name = ?
                    AND e.event_name = 'page_view'
                GROUP BY e.variants
                ORDER BY conversion_rate DESC";
        $stmt = $this->db->prepare($sql);
        $stmt->execute([$testName]);
        $results = [];
        while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
            $variants = json_decode($row['variants'], true);
            $results[] = [
                'variants' => $variants,
                'visitors' => $row['visitors'],
                'conversions' => $row['conversions'],
                'conversion_rate' => round($row['conversion_rate'], 2)
            ];
        }
        return $results;
    }
    /**
     * 获取每个变量的表现
     */
    public function getVariablePerformance($testName) {
        $config = $this->getTestConfig($testName);
        $variablePerformance = [];
        foreach ($config['variables'] as $varName => $variable) {
            $performance = [];
            foreach ($variable['variants'] as $variantKey => $variantValue) {
                $sql = "SELECT 
                            COUNT(DISTINCT e.user_id) as visitors,
                            COUNT(DISTINCT c.user_id) as conversions
                        FROM mvt_events e
                        LEFT JOIN mvt_conversions c 
                            ON e.test_name = c.test_name 
                            AND e.user_id = c.user_id
                        WHERE e.test_name = ?
                            AND JSON_EXTRACT(e.variants, '$.\"$varName\"') = ?
                            AND e.event_name = 'page_view'";
                $stmt = $this->db->prepare($sql);
                $stmt->execute([$testName, $variantKey]);
                $data = $stmt->fetch(PDO::FETCH_ASSOC);
                $performance[$variantKey] = [
                    'value' => $variantValue,
                    'visitors' => $data['visitors'],
                    'conversions' => $data['conversions'],
                    'conversion_rate' => $data['visitors'] > 0 ? 
                        round($data['conversions'] / $data['visitors'] * 100, 2) : 0
                ];
            }
            $variablePerformance[$varName] = $performance;
        }
        return $variablePerformance;
    }
    /**
     * 统计显著性检查(简化版)
     */
    public function calculateSignificance($testName) {
        $results = $this->getTestResults($testName);
        if (count($results) < 2) {
            return null;
        }
        // 以第一个结果为基准
        $baseline = $results[0];
        $significance = [];
        for ($i = 1; $i < count($results); $i++) {
            $variant = $results[$i];
            $zScore = $this->calculateZScore(
                $baseline['conversion_rate'] / 100,
                $variant['conversion_rate'] / 100,
                $baseline['visitors'],
                $variant['visitors']
            );
            $significance[$i] = [
                'variants' => $variant['variants'],
                'z_score' => round($zScore, 2),
                'p_value' => $this->calculatePValue($zScore)
            ];
        }
        return $significance;
    }
    private function calculateZScore($p1, $p2, $n1, $n2) {
        $p = ($p1 * $n1 + $p2 * $n2) / ($n1 + $n2);
        $se = sqrt($p * (1 - $p) * (1/$n1 + 1/$n2));
        return ($p1 - $p2) / $se;
    }
    private function calculatePValue($zScore) {
        // 简化实现,实际应用应使用更精确的算法
        return 2 * (1 - 0.5 * (1 + erf($zScore / sqrt(2))));
    }
}

高级优化建议

1 缓存机制

<?php
// 使用Redis缓存用户分配
class MVTEngineWithCache extends MVTEngine {
    private $redis;
    public function assignVariants($testName) {
        $cacheKey = "mvt:{$this->userId}:{$testName}";
        // 检查缓存
        $cached = $this->redis->get($cacheKey);
        if ($cached) {
            return json_decode($cached, true);
        }
        // 调用父类方法
        $variants = parent::assignVariants($testName);
        // 设置缓存(24小时过期)
        $this->redis->setex($cacheKey, 86400, json_encode($variants));
        return $variants;
    }
}

2 流量分配控制

<?php
// 使用MurmurHash实现一致性分配
function consistentAssignment($userId, $testName, $totalVariants) {
    $hash = hash('murmur3f', $userId . '_' . $testName);
    // 将hash转换为整数
    $intHash = hexdec(substr($hash, 0, 8));
    return $intHash % $totalVariants;
}

3 A/B/n 测试支持

<?php
// 扩展以支持A/B/n测试(多变量测试的特例)
class ABnTestEngine extends MVTEngine {
    public function assignVariant($testName) {
        if ($this->getExistingAssignment($testName)) {
            return parent::assignVariants($testName);
        }
        // 简化为单一变量测试
        return parent::assignVariants($testName);
    }
}

最佳实践

  1. 性能优化:使用缓存减少数据库查询
  2. 防止污染:确保用户始终看到相同的变体组合
  3. 数据质量:过滤机器人流量和无效数据
  4. 实时监控:监控测试效果,及时发现异常
  5. 渐进式部署:先小流量测试,逐步扩大

这个实现方案提供了完整的多变量测试功能,可以根据实际需求进行调整和扩展。

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