339 lines
11 KiB
TypeScript
339 lines
11 KiB
TypeScript
|
|
import { Logger } from '../utils/Logger';
|
|||
|
|
|
|||
|
|
export interface TestResult {
|
|||
|
|
testId: string;
|
|||
|
|
variations: VariationResult[];
|
|||
|
|
metrics: {
|
|||
|
|
[metric: string]: number;
|
|||
|
|
};
|
|||
|
|
startDate: string;
|
|||
|
|
endDate: string;
|
|||
|
|
sampleSize: number;
|
|||
|
|
statisticalSignificance: number;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export interface VariationResult {
|
|||
|
|
id: string;
|
|||
|
|
name: string;
|
|||
|
|
metrics: {
|
|||
|
|
[metric: string]: number;
|
|||
|
|
};
|
|||
|
|
sampleSize: number;
|
|||
|
|
conversionRate?: number;
|
|||
|
|
engagementRate?: number;
|
|||
|
|
revenuePerUser?: number;
|
|||
|
|
retentionRate?: number;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export interface OptimizationGoal {
|
|||
|
|
type: 'maximize' | 'minimize';
|
|||
|
|
metric: string;
|
|||
|
|
targetValue?: number;
|
|||
|
|
weight?: number;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export interface OptimizationRecommendation {
|
|||
|
|
recommendedVariation: string;
|
|||
|
|
confidence: number;
|
|||
|
|
expectedImprovement: number;
|
|||
|
|
optimizationActions: OptimizationAction[];
|
|||
|
|
riskAssessment: 'low' | 'medium' | 'high';
|
|||
|
|
implementationSteps: string[];
|
|||
|
|
followUpTests: FollowUpTest[];
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export interface OptimizationAction {
|
|||
|
|
id: string;
|
|||
|
|
description: string;
|
|||
|
|
priority: 'high' | 'medium' | 'low';
|
|||
|
|
expectedImpact: number;
|
|||
|
|
implementationEffort: 'low' | 'medium' | 'high';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export interface FollowUpTest {
|
|||
|
|
testName: string;
|
|||
|
|
description: string;
|
|||
|
|
recommendedTiming: string;
|
|||
|
|
estimatedDuration: number;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export class ABTestOptimizationService {
|
|||
|
|
private logger = new Logger('ABTestOptimizationService');
|
|||
|
|
|
|||
|
|
async optimizeTest(
|
|||
|
|
testResult: TestResult,
|
|||
|
|
optimizationGoals: OptimizationGoal[],
|
|||
|
|
traceId: string
|
|||
|
|
): Promise<OptimizationRecommendation> {
|
|||
|
|
try {
|
|||
|
|
this.logger.info('开始优化A/B测试', {
|
|||
|
|
testId: testResult.testId,
|
|||
|
|
variationCount: testResult.variations.length,
|
|||
|
|
traceId
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
const bestVariation = this.identifyBestVariation(testResult, optimizationGoals);
|
|||
|
|
const confidence = this.calculateConfidence(testResult, bestVariation.id);
|
|||
|
|
const expectedImprovement = this.calculateExpectedImprovement(testResult, bestVariation.id);
|
|||
|
|
const optimizationActions = this.generateOptimizationActions(testResult, bestVariation, optimizationGoals);
|
|||
|
|
const risk = this.assessRisk(testResult, bestVariation, optimizationGoals);
|
|||
|
|
const implementationSteps = this.generateImplementationSteps(optimizationActions);
|
|||
|
|
const followUpTests = this.recommendFollowUpTests(testResult, bestVariation, optimizationGoals);
|
|||
|
|
|
|||
|
|
const recommendation: OptimizationRecommendation = {
|
|||
|
|
recommendedVariation: bestVariation.id,
|
|||
|
|
confidence,
|
|||
|
|
expectedImprovement,
|
|||
|
|
optimizationActions,
|
|||
|
|
riskAssessment: risk,
|
|||
|
|
implementationSteps,
|
|||
|
|
followUpTests
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
this.logger.info('A/B测试优化完成', {
|
|||
|
|
recommendedVariation: bestVariation.id,
|
|||
|
|
confidence,
|
|||
|
|
expectedImprovement,
|
|||
|
|
traceId
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
return recommendation;
|
|||
|
|
} catch (error) {
|
|||
|
|
this.logger.error('优化A/B测试失败', {
|
|||
|
|
error: error instanceof Error ? error.message : '未知错误',
|
|||
|
|
traceId
|
|||
|
|
});
|
|||
|
|
throw error;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private identifyBestVariation(testResult: TestResult, optimizationGoals: OptimizationGoal[]): VariationResult {
|
|||
|
|
let bestVariation: VariationResult = testResult.variations[0];
|
|||
|
|
let bestScore = -Infinity;
|
|||
|
|
|
|||
|
|
testResult.variations.forEach(variation => {
|
|||
|
|
let score = 0;
|
|||
|
|
|
|||
|
|
optimizationGoals.forEach(goal => {
|
|||
|
|
const value = variation.metrics[goal.metric] || 0;
|
|||
|
|
const weight = goal.weight || 1;
|
|||
|
|
|
|||
|
|
if (goal.type === 'maximize') {
|
|||
|
|
score += value * weight;
|
|||
|
|
} else {
|
|||
|
|
score += (1 / (value + 0.001)) * weight;
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
if (score > bestScore) {
|
|||
|
|
bestScore = score;
|
|||
|
|
bestVariation = variation;
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
return bestVariation;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private calculateConfidence(testResult: TestResult, bestVariationId: string): number {
|
|||
|
|
const bestVariation = testResult.variations.find(v => v.id === bestVariationId);
|
|||
|
|
if (!bestVariation) return 0;
|
|||
|
|
|
|||
|
|
const otherVariations = testResult.variations.filter(v => v.id !== bestVariationId);
|
|||
|
|
const significantWins = otherVariations.filter(other => {
|
|||
|
|
return this.isStatisticallySignificant(bestVariation!, other);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
return Math.min(1, significantWins.length / otherVariations.length * 0.8 + 0.2);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private calculateExpectedImprovement(testResult: TestResult, bestVariationId: string): number {
|
|||
|
|
const bestVariation = testResult.variations.find(v => v.id === bestVariationId);
|
|||
|
|
if (!bestVariation) return 0;
|
|||
|
|
|
|||
|
|
const otherVariations = testResult.variations.filter(v => v.id !== bestVariationId);
|
|||
|
|
if (otherVariations.length === 0) return 0;
|
|||
|
|
|
|||
|
|
const averagePerformance = otherVariations.reduce((sum, v) => {
|
|||
|
|
const metricValues = Object.values(v.metrics);
|
|||
|
|
return sum + metricValues.reduce((sum, val) => sum + val, 0) / metricValues.length;
|
|||
|
|
}, 0) / otherVariations.length;
|
|||
|
|
|
|||
|
|
const bestPerformance = Object.values(bestVariation.metrics).reduce((sum, val) => sum + val, 0) / Object.values(bestVariation.metrics).length;
|
|||
|
|
|
|||
|
|
return (bestPerformance - averagePerformance) / averagePerformance;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private generateOptimizationActions(
|
|||
|
|
testResult: TestResult,
|
|||
|
|
bestVariation: VariationResult,
|
|||
|
|
optimizationGoals: OptimizationGoal[]
|
|||
|
|
): OptimizationAction[] {
|
|||
|
|
const actions: OptimizationAction[] = [];
|
|||
|
|
|
|||
|
|
optimizationGoals.forEach(goal => {
|
|||
|
|
const bestValue = bestVariation.metrics[goal.metric];
|
|||
|
|
if (bestValue === undefined) return;
|
|||
|
|
|
|||
|
|
const otherVariations = testResult.variations.filter(v => v.id !== bestVariation.id);
|
|||
|
|
const averageValue = otherVariations.reduce((sum, v) => sum + (v.metrics[goal.metric] || 0), 0) / otherVariations.length;
|
|||
|
|
const improvement = (bestValue - averageValue) / averageValue;
|
|||
|
|
|
|||
|
|
if (Math.abs(improvement) > 0.05) {
|
|||
|
|
actions.push({
|
|||
|
|
id: `action_${goal.metric}`,
|
|||
|
|
description: `优化 ${goal.metric} 指标,采用 ${bestVariation.name} 的策略`,
|
|||
|
|
priority: Math.abs(improvement) > 0.15 ? 'high' : Math.abs(improvement) > 0.08 ? 'medium' : 'low',
|
|||
|
|
expectedImpact: Math.abs(improvement),
|
|||
|
|
implementationEffort: Math.abs(improvement) > 0.15 ? 'medium' : 'low'
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
if (actions.length === 0) {
|
|||
|
|
actions.push({
|
|||
|
|
id: 'action_no_change',
|
|||
|
|
description: '保持当前策略,继续监控性能',
|
|||
|
|
priority: 'low',
|
|||
|
|
expectedImpact: 0,
|
|||
|
|
implementationEffort: 'low'
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return actions.sort((a, b) => {
|
|||
|
|
const priorityOrder = { high: 3, medium: 2, low: 1 };
|
|||
|
|
return priorityOrder[b.priority] - priorityOrder[a.priority];
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private assessRisk(
|
|||
|
|
testResult: TestResult,
|
|||
|
|
bestVariation: VariationResult,
|
|||
|
|
optimizationGoals: OptimizationGoal[]
|
|||
|
|
): 'low' | 'medium' | 'high' {
|
|||
|
|
let riskScore = 0;
|
|||
|
|
|
|||
|
|
if (testResult.sampleSize < 500) riskScore += 2;
|
|||
|
|
else if (testResult.sampleSize < 1000) riskScore += 1;
|
|||
|
|
|
|||
|
|
if (testResult.statisticalSignificance > 0.1) riskScore += 2;
|
|||
|
|
else if (testResult.statisticalSignificance > 0.05) riskScore += 1;
|
|||
|
|
|
|||
|
|
const otherVariations = testResult.variations.filter(v => v.id !== bestVariation.id);
|
|||
|
|
const closeCompetitors = otherVariations.filter(other => {
|
|||
|
|
const bestScore = Object.values(bestVariation.metrics).reduce((sum, val) => sum + val, 0);
|
|||
|
|
const otherScore = Object.values(other.metrics).reduce((sum, val) => sum + val, 0);
|
|||
|
|
return (otherScore / bestScore) > 0.95;
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
if (closeCompetitors.length > 0) riskScore += 1;
|
|||
|
|
|
|||
|
|
const hasRevenueGoal = optimizationGoals.some(goal => goal.metric.includes('revenue'));
|
|||
|
|
if (hasRevenueGoal) riskScore += 1;
|
|||
|
|
|
|||
|
|
if (riskScore >= 5) return 'high';
|
|||
|
|
if (riskScore >= 3) return 'medium';
|
|||
|
|
return 'low';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private generateImplementationSteps(actions: OptimizationAction[]): string[] {
|
|||
|
|
const steps: string[] = [];
|
|||
|
|
|
|||
|
|
steps.push('1. 审核优化建议,确认推荐的变体');
|
|||
|
|
steps.push('2. 制定实施计划,包括时间线和负责人');
|
|||
|
|
|
|||
|
|
actions.forEach((action, index) => {
|
|||
|
|
steps.push(`${index + 3}. 实施 ${action.description}(优先级:${action.priority})`);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
steps.push(`${actions.length + 3}. 监控实施后的性能指标`);
|
|||
|
|
steps.push(`${actions.length + 4}. 记录实施结果,用于后续分析`);
|
|||
|
|
|
|||
|
|
return steps;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private recommendFollowUpTests(
|
|||
|
|
testResult: TestResult,
|
|||
|
|
bestVariation: VariationResult,
|
|||
|
|
optimizationGoals: OptimizationGoal[]
|
|||
|
|
): FollowUpTest[] {
|
|||
|
|
const tests: FollowUpTest[] = [];
|
|||
|
|
|
|||
|
|
if (optimizationGoals.some(goal => goal.type === 'maximize' && goal.metric.includes('conversion'))) {
|
|||
|
|
tests.push({
|
|||
|
|
testName: '转化率优化深度测试',
|
|||
|
|
description: '进一步优化已识别的高转化率策略',
|
|||
|
|
recommendedTiming: '2周后',
|
|||
|
|
estimatedDuration: 7
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (optimizationGoals.some(goal => goal.type === 'maximize' && goal.metric.includes('revenue'))) {
|
|||
|
|
tests.push({
|
|||
|
|
testName: '收入优化扩展测试',
|
|||
|
|
description: '测试不同定价策略和促销组合',
|
|||
|
|
recommendedTiming: '3周后',
|
|||
|
|
estimatedDuration: 14
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (tests.length === 0) {
|
|||
|
|
tests.push({
|
|||
|
|
testName: '性能监控测试',
|
|||
|
|
description: '持续监控实施后的性能变化',
|
|||
|
|
recommendedTiming: '1周后',
|
|||
|
|
estimatedDuration: 7
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return tests;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private isStatisticallySignificant(variationA: VariationResult, variationB: VariationResult): boolean {
|
|||
|
|
const metricsA = Object.values(variationA.metrics);
|
|||
|
|
const metricsB = Object.values(variationB.metrics);
|
|||
|
|
|
|||
|
|
if (metricsA.length === 0 || metricsB.length === 0) return false;
|
|||
|
|
|
|||
|
|
const meanA = metricsA.reduce((sum, val) => sum + val, 0) / metricsA.length;
|
|||
|
|
const meanB = metricsB.reduce((sum, val) => sum + val, 0) / metricsB.length;
|
|||
|
|
|
|||
|
|
const varianceA = metricsA.reduce((sum, val) => sum + Math.pow(val - meanA, 2), 0) / metricsA.length;
|
|||
|
|
const varianceB = metricsB.reduce((sum, val) => sum + Math.pow(val - meanB, 2), 0) / metricsB.length;
|
|||
|
|
|
|||
|
|
const standardError = Math.sqrt(varianceA / metricsA.length + varianceB / metricsB.length);
|
|||
|
|
const zScore = Math.abs(meanA - meanB) / standardError;
|
|||
|
|
|
|||
|
|
return zScore > 1.96; // 95% confidence
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async validateOptimization(recommendation: OptimizationRecommendation, traceId: string): Promise<{ valid: boolean; warnings: string[] }> {
|
|||
|
|
const warnings: string[] = [];
|
|||
|
|
|
|||
|
|
if (recommendation.confidence < 0.7) {
|
|||
|
|
warnings.push('置信度较低,建议增加样本量或延长测试时间');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (recommendation.expectedImprovement < 0.05) {
|
|||
|
|
warnings.push('预期改进较小,可能不值得实施');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (recommendation.riskAssessment === 'high') {
|
|||
|
|
warnings.push('风险评估为高,建议谨慎实施');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (recommendation.optimizationActions.length === 0) {
|
|||
|
|
warnings.push('未生成优化行动,建议重新评估测试结果');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
this.logger.info('优化建议验证完成', {
|
|||
|
|
warningCount: warnings.length,
|
|||
|
|
traceId
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
valid: warnings.length < 3,
|
|||
|
|
warnings
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
}
|