本文目录导读:

我来详细解释PHP实现AB测试的几种常见方法:
基本AB测试实现
基于用户ID或Session的随机分配
<?php
class ABTest {
private $redis;
private $experiments = [];
public function __construct($redis = null) {
$this->redis = $redis ?: new Redis();
}
// 注册实验
public function registerExperiment($name, $variants) {
$this->experiments[$name] = $variants;
}
// 获取用户分组
public function getVariant($experimentName, $userId) {
if (!isset($this->experiments[$experimentName])) {
return null;
}
$variants = $this->experiments[$experimentName];
$cacheKey = "abtest:{$experimentName}:{$userId}";
// 检查用户是否已有分组
$variant = $this->redis->get($cacheKey);
if ($variant !== false) {
return $variant;
}
// 使用用户ID进行一致性哈希分配
$hash = crc32($userId . $experimentName);
$index = abs($hash) % count($variants);
$variant = $variants[$index];
// 缓存分组结果
$this->redis->setex($cacheKey, 86400 * 30, $variant);
return $variant;
}
}
// 使用示例
$abTest = new ABTest();
$abTest->registerExperiment('button_color', ['red', 'blue', 'green']);
$userId = $_SESSION['user_id'] ?? session_id();
$buttonColor = $abTest->getVariant('button_color', $userId);
基于Cookie的AB测试
<?php
class CookieABTest {
private $cookiePrefix = 'abtest_';
private $cookieExpire = 2592000; // 30天
// 分配变体
public function assignVariant($experimentName, $variants) {
$cookieName = $this->cookiePrefix . $experimentName;
// 检查Cookie中是否有分组
if (isset($_COOKIE[$cookieName])) {
return $_COOKIE[$cookieName];
}
// 随机分配变体
$variant = $variants[array_rand($variants)];
// 设置Cookie
setcookie($cookieName, $variant, time() + $this->cookieExpire, '/');
return $variant;
}
// 获取变体
public function getVariant($experimentName) {
$cookieName = $this->cookiePrefix . $experimentName;
return $_COOKIE[$cookieName] ?? null;
}
}
// 使用示例
$cookieTest = new CookieABTest();
$landingPage = $cookieTest->assignVariant('landing_page', [
'page_a', 'page_b'
]);
基于数据库的AB测试系统
<?php
// 1. 数据表结构
/*
CREATE TABLE ab_experiments (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(100) UNIQUE,
status ENUM('draft', 'running', 'paused', 'completed'),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE ab_variants (
id INT PRIMARY KEY AUTO_INCREMENT,
experiment_id INT,
name VARCHAR(50),
traffic_ratio DECIMAL(5,2), -- 流量比例
FOREIGN KEY (experiment_id) REFERENCES ab_experiments(id)
);
CREATE TABLE ab_assignments (
id INT PRIMARY KEY AUTO_INCREMENT,
experiment_id INT,
user_id VARCHAR(100),
variant_id INT,
assigned_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY (experiment_id, user_id)
);
CREATE TABLE ab_events (
id INT PRIMARY KEY AUTO_INCREMENT,
assignment_id INT,
event_type VARCHAR(50), -- 'view', 'click', 'conversion'
event_data JSON,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
*/
class DatabaseABTest {
private $db;
private $experimentCache = [];
public function __construct(PDO $db) {
$this->db = $db;
}
// 获取用户变体
public function getVariant($experimentName, $userId) {
// 获取实验信息
$experiment = $this->getExperiment($experimentName);
if (!$experiment || $experiment['status'] !== 'running') {
return null;
}
// 检查是否已有分配
$existingAssignment = $this->getExistingAssignment($experiment['id'], $userId);
if ($existingAssignment) {
return $existingAssignment;
}
// 分配新变体
return $this->assignVariant($experiment, $userId);
}
// 记录事件
public function trackEvent($experimentName, $userId, $eventType, $eventData = []) {
$experiment = $this->getExperiment($experimentName);
$assignment = $this->getExistingAssignment($experiment['id'], $userId);
if ($assignment) {
$stmt = $this->db->prepare("
INSERT INTO ab_events (assignment_id, event_type, event_data)
VALUES (?, ?, ?)
");
$stmt->execute([
$assignment['id'],
$eventType,
json_encode($eventData)
]);
}
}
private function getExperiment($name) {
if (!isset($this->experimentCache[$name])) {
$stmt = $this->db->prepare("SELECT * FROM ab_experiments WHERE name = ?");
$stmt->execute([$name]);
$experiment = $stmt->fetch(PDO::FETCH_ASSOC);
if ($experiment) {
// 获取变体
$stmt = $this->db->prepare("SELECT * FROM ab_variants WHERE experiment_id = ?");
$stmt->execute([$experiment['id']]);
$experiment['variants'] = $stmt->fetchAll(PDO::FETCH_ASSOC);
$this->experimentCache[$name] = $experiment;
}
}
return $this->experimentCache[$name] ?? null;
}
private function getExistingAssignment($experimentId, $userId) {
$stmt = $this->db->prepare("
SELECT a.*, v.name as variant_name
FROM ab_assignments a
JOIN ab_variants v ON a.variant_id = v.id
WHERE a.experiment_id = ? AND a.user_id = ?
");
$stmt->execute([$experimentId, $userId]);
return $stmt->fetch(PDO::FETCH_ASSOC);
}
private function assignVariant($experiment, $userId) {
// 根据流量比例分配
$rand = mt_rand(1, 100);
$cumulative = 0;
foreach ($experiment['variants'] as $variant) {
$cumulative += $variant['traffic_ratio'];
if ($rand <= $cumulative) {
// 记录分配
$stmt = $this->db->prepare("
INSERT INTO ab_assignments (experiment_id, user_id, variant_id)
VALUES (?, ?, ?)
");
$stmt->execute([$experiment['id'], $userId, $variant['id']]);
return [
'id' => $this->db->lastInsertId(),
'variant_name' => $variant['name']
];
}
}
return null;
}
}
完整AB测试框架示例
<?php
// ABTestManager.php - 完整管理类
class ABTestManager {
private $experiments = [];
private $results = [];
private $logger;
public function __construct(Logger $logger = null) {
$this->logger = $logger ?: new NullLogger();
}
// 创建实验
public function createExperiment($name, $config) {
$this->experiments[$name] = [
'name' => $name,
'variants' => $config['variants'] ?? ['control', 'variant'],
'metrics' => $config['metrics'] ?? ['conversion'],
'sample_size' => $config['sample_size'] ?? 1000,
'traffic_percentage' => $config['traffic_percentage'] ?? 100,
'created_at' => time()
];
$this->logger->info("Created experiment: {$name}");
return $this;
}
// 运行实验
public function runExperiment($name, $context) {
if (!isset($this->experiments[$name])) {
throw new Exception("Experiment {$name} not found");
}
$experiment = $this->experiments[$name];
$variant = $this->assignVariant($experiment, $context);
// 记录实验参与
$this->recordParticipation($name, $variant, $context);
return $variant;
}
// 跟踪指标
public function trackMetric($experimentName, $metric, $value, $context) {
$key = "{$experimentName}:{$metric}";
if (!isset($this->results[$key])) {
$this->results[$key] = [];
}
$variant = $this->getUserVariant($experimentName, $context);
if ($variant) {
$this->results[$key][$variant][] = $value;
}
}
// 分析结果
public function analyzeResults($experimentName) {
$experiment = $this->experiments[$experimentName];
$analysis = [];
foreach ($experiment['metrics'] as $metric) {
$key = "{$experimentName}:{$metric}";
$data = $this->results[$key] ?? [];
foreach ($experiment['variants'] as $variant) {
$values = $data[$variant] ?? [];
$analysis[$variant][$metric] = [
'count' => count($values),
'mean' => count($values) > 0 ? array_sum($values) / count($values) : 0,
'sum' => array_sum($values),
'values' => $values
];
}
// 计算统计显著性
$analysis['significance'] = $this->calculateSignificance($analysis);
}
return $analysis;
}
private function assignVariant($experiment, $context) {
$userId = $context['user_id'] ?? session_id();
$hash = crc32($userId . $experiment['name']);
// 检查流量百分比
if (($hash % 100) >= $experiment['traffic_percentage']) {
return null; // 不在实验范围内
}
$index = abs($hash) % count($experiment['variants']);
return $experiment['variants'][$index];
}
private function recordParticipation($experimentName, $variant, $context) {
$this->logger->info("User {$context['user_id']} in experiment {$experimentName}, variant: {$variant}");
}
private function getUserVariant($experimentName, $context) {
// 从缓存或数据库获取用户分组
$userId = $context['user_id'] ?? session_id();
$hash = crc32($userId . $experimentName);
$experiment = $this->experiments[$experimentName];
if (($hash % 100) >= $experiment['traffic_percentage']) {
return null;
}
$index = abs($hash) % count($experiment['variants']);
return $experiment['variants'][$index];
}
private function calculateSignificance($analysis) {
// 简单的卡方检验实现
// 实际应用中可以使用更复杂的统计方法
return [
'method' => 'chi_square',
'p_value' => 0.05, // 示例值
'significant' => true
];
}
}
// 使用示例
$abManager = new ABTestManager();
// 创建实验
$abManager->createExperiment('pricing_page_v2', [
'variants' => ['current', 'new_design'],
'metrics' => ['click_rate', 'conversion_rate', 'revenue'],
'sample_size' => 5000,
'traffic_percentage' => 50
]);
// 分配用户并跟踪
$context = [
'user_id' => 'user_123',
'page' => '/pricing',
'timestamp' => time()
];
$variant = $abManager->runExperiment('pricing_page_v2', $context);
// 跟踪用户行为
$abManager->trackMetric('pricing_page_v2', 'click_rate', 1, $context);
$abManager->trackMetric('pricing_page_v2', 'conversion_rate', 0, $context);
// 分析结果
$results = $abManager->analyzeResults('pricing_page_v2');
最佳实践建议
注意事项:
- 样本量计算:确保足够的样本量来获得统计显著性
- 随机化:使用一致的哈希算法确保用户分组稳定
- 数据隔离:不同实验应该独立运行,避免相互影响
- 监控和告警:实时监控实验指标,发现异常及时停止
- 长期影响:考虑实验可能带来的长期影响
性能优化:
// 使用缓存减少数据库查询
class OptimizedABTest {
private $cache;
private $ttl = 3600;
public function getVariant($experiment, $userId) {
$cacheKey = "abtest:{$experiment}:{$userId}";
// 尝试从缓存获取
if ($variant = $this->cache->get($cacheKey)) {
return $variant;
}
// 计算并缓存
$variant = $this->calculateVariant($experiment, $userId);
$this->cache->set($cacheKey, $variant, $this->ttl);
return $variant;
}
}
这就是PHP实现AB测试的完整方案,根据你的具体需求选择合适的方法,对于简单场景可以使用Cookie或Session,对于复杂场景建议使用数据库+缓存的方式。