'fraud-detector', 'version_a' => '1.0.0', 'version_b' => '2.0.0', 'traffic_split_a' => 0.5, 'primary_metric' => 'accuracy', 'minimum_improvement' => 0.05, ], )] #[ApiResponse( statusCode: 201, description: 'A/B test created successfully', example: [ 'test_id' => 'test_123', 'model_name' => 'fraud-detector', 'version_a' => '1.0.0', 'version_b' => '2.0.0', 'traffic_split' => [ 'version_a' => 0.5, 'version_b' => 0.5, ], 'status' => 'running', 'created_at' => '2024-01-01T00:00:00Z', ], )] #[ApiResponse( statusCode: 400, description: 'Invalid test configuration', )] public function startTest(HttpRequest $request): JsonResult { try { $data = $request->parsedBody->toArray(); // Validate required fields if (!isset($data['model_name'], $data['version_a'], $data['version_b'])) { return new JsonResult([ 'error' => 'Missing required fields', 'required' => ['model_name', 'version_a', 'version_b'], ], Status::BAD_REQUEST); } // Parse versions $versionA = Version::fromString($data['version_a']); $versionB = Version::fromString($data['version_b']); // Verify models exist $metadataA = $this->registry->get($data['model_name'], $versionA); $metadataB = $this->registry->get($data['model_name'], $versionB); if ($metadataA === null) { return new JsonResult([ 'error' => 'Version A not found', 'model_name' => $data['model_name'], 'version' => $data['version_a'], ], Status::NOT_FOUND); } if ($metadataB === null) { return new JsonResult([ 'error' => 'Version B not found', 'model_name' => $data['model_name'], 'version' => $data['version_b'], ], Status::NOT_FOUND); } // Create A/B test config $config = new ABTestConfig( modelName: $data['model_name'], versionA: $versionA, versionB: $versionB, trafficSplitA: (float) ($data['traffic_split_a'] ?? 0.5), primaryMetric: $data['primary_metric'] ?? 'accuracy', minimumImprovement: (float) ($data['minimum_improvement'] ?? 0.05), significanceLevel: (float) ($data['significance_level'] ?? 0.05) ); // Generate test ID (in production, store in database) $testId = 'test_' . bin2hex(random_bytes(8)); return new JsonResult([ 'test_id' => $testId, 'model_name' => $config->modelName, 'version_a' => $config->versionA->toString(), 'version_b' => $config->versionB->toString(), 'traffic_split' => [ 'version_a' => $config->trafficSplitA, 'version_b' => 1.0 - $config->trafficSplitA, ], 'primary_metric' => $config->primaryMetric, 'minimum_improvement' => $config->minimumImprovement, 'status' => 'running', 'description' => $config->getDescription(), 'created_at' => date('c'), ], Status::CREATED); } catch (\InvalidArgumentException $e) { return new JsonResult([ 'error' => 'Invalid test configuration', 'message' => $e->getMessage(), ], Status::BAD_REQUEST); } } #[Route(path: '/api/ml/ab-test/compare', method: Method::POST)] #[ApiEndpoint( summary: 'Compare model versions', description: 'Compare performance of two model versions and get winner recommendation', tags: ['Machine Learning'], )] #[ApiRequestBody( description: 'Model comparison configuration', required: true, example: [ 'model_name' => 'fraud-detector', 'version_a' => '1.0.0', 'version_b' => '2.0.0', 'primary_metric' => 'f1_score', 'minimum_improvement' => 0.05, ], )] #[ApiResponse( statusCode: 200, description: 'Comparison completed successfully', example: [ 'winner' => 'B', 'statistically_significant' => true, 'metrics_difference' => [ 'accuracy' => 0.07, 'f1_score' => 0.08, ], 'primary_metric_improvement' => 8.5, 'recommendation' => 'Version B wins with 8.5% improvement - deploy new version', 'summary' => 'Version B significantly outperforms Version A', ], )] #[ApiResponse( statusCode: 404, description: 'Model version not found', )] public function compareVersions(HttpRequest $request): JsonResult { try { $data = $request->parsedBody->toArray(); if (!isset($data['model_name'], $data['version_a'], $data['version_b'])) { return new JsonResult([ 'error' => 'Missing required fields', 'required' => ['model_name', 'version_a', 'version_b'], ], Status::BAD_REQUEST); } $versionA = Version::fromString($data['version_a']); $versionB = Version::fromString($data['version_b']); $config = new ABTestConfig( modelName: $data['model_name'], versionA: $versionA, versionB: $versionB, trafficSplitA: 0.5, primaryMetric: $data['primary_metric'] ?? 'accuracy', minimumImprovement: (float) ($data['minimum_improvement'] ?? 0.05) ); // Run comparison $result = $this->abTesting->runTest($config); return new JsonResult([ 'winner' => $result->winner, 'statistically_significant' => $result->isStatisticallySignificant, 'metrics_difference' => $result->metricsDifference, 'primary_metric_improvement' => $result->getPrimaryMetricImprovementPercent(), 'recommendation' => $result->recommendation, 'summary' => $result->getSummary(), 'should_deploy_version_b' => $result->shouldDeployVersionB(), 'is_inconclusive' => $result->isInconclusive(), ]); } catch (\InvalidArgumentException $e) { return new JsonResult([ 'error' => 'Invalid comparison parameters', 'message' => $e->getMessage(), ], Status::BAD_REQUEST); } } #[Route(path: '/api/ml/ab-test/rollout-plan', method: Method::POST)] #[ApiEndpoint( summary: 'Generate rollout plan', description: 'Generate a gradual rollout plan for deploying a new model version', tags: ['Machine Learning'], )] #[ApiRequestBody( description: 'Rollout configuration', required: true, example: [ 'model_name' => 'fraud-detector', 'current_version' => '1.0.0', 'new_version' => '2.0.0', 'steps' => 5, ], )] #[ApiResponse( statusCode: 200, description: 'Rollout plan generated successfully', example: [ 'model_name' => 'fraud-detector', 'current_version' => '1.0.0', 'new_version' => '2.0.0', 'rollout_stages' => [ [ 'stage' => 1, 'current_version_traffic' => 80, 'new_version_traffic' => 20, ], [ 'stage' => 2, 'current_version_traffic' => 60, 'new_version_traffic' => 40, ], ], ], )] public function generateRolloutPlan(HttpRequest $request): JsonResult { try { $data = $request->parsedBody->toArray(); if (!isset($data['model_name'], $data['current_version'], $data['new_version'])) { return new JsonResult([ 'error' => 'Missing required fields', 'required' => ['model_name', 'current_version', 'new_version'], ], Status::BAD_REQUEST); } $steps = (int) ($data['steps'] ?? 5); if ($steps < 2 || $steps > 10) { return new JsonResult([ 'error' => 'Steps must be between 2 and 10', ], Status::BAD_REQUEST); } // Generate rollout plan $plan = $this->abTesting->generateRolloutPlan($steps); // Format response $stages = []; foreach ($plan as $stage => $newVersionTraffic) { $stages[] = [ 'stage' => $stage, 'current_version_traffic' => (int) ((1.0 - $newVersionTraffic) * 100), 'new_version_traffic' => (int) ($newVersionTraffic * 100), ]; } return new JsonResult([ 'model_name' => $data['model_name'], 'current_version' => $data['current_version'], 'new_version' => $data['new_version'], 'total_stages' => $steps, 'rollout_stages' => $stages, 'recommendation' => 'Monitor performance at each stage before proceeding to next', ]); } catch (\InvalidArgumentException $e) { return new JsonResult([ 'error' => 'Invalid rollout configuration', 'message' => $e->getMessage(), ], Status::BAD_REQUEST); } } #[Route(path: '/api/ml/ab-test/sample-size', method: Method::GET)] #[ApiEndpoint( summary: 'Calculate required sample size', description: 'Calculate the required sample size for statistically significant A/B test', tags: ['Machine Learning'], )] #[ApiParameter( name: 'confidence_level', in: 'query', description: 'Confidence level (0.90, 0.95, 0.99)', required: false, type: 'number', example: 0.95, )] #[ApiParameter( name: 'margin_of_error', in: 'query', description: 'Margin of error (typically 0.01-0.10)', required: false, type: 'number', example: 0.05, )] #[ApiResponse( statusCode: 200, description: 'Sample size calculated successfully', example: [ 'required_samples_per_version' => 385, 'total_samples_needed' => 770, 'confidence_level' => 0.95, 'margin_of_error' => 0.05, 'recommendation' => 'Collect at least 385 predictions per version', ], )] public function calculateSampleSize(HttpRequest $request): JsonResult { $confidenceLevel = (float) ($request->queryParameters['confidence_level'] ?? 0.95); $marginOfError = (float) ($request->queryParameters['margin_of_error'] ?? 0.05); // Validate parameters if ($confidenceLevel < 0.5 || $confidenceLevel > 0.99) { return new JsonResult([ 'error' => 'Confidence level must be between 0.5 and 0.99', ], Status::BAD_REQUEST); } if ($marginOfError < 0.01 || $marginOfError > 0.20) { return new JsonResult([ 'error' => 'Margin of error must be between 0.01 and 0.20', ], Status::BAD_REQUEST); } // Calculate sample size $samplesPerVersion = $this->abTesting->calculateRequiredSampleSize( $confidenceLevel, $marginOfError ); return new JsonResult([ 'required_samples_per_version' => $samplesPerVersion, 'total_samples_needed' => $samplesPerVersion * 2, 'confidence_level' => $confidenceLevel, 'margin_of_error' => $marginOfError, 'confidence_level_percent' => ($confidenceLevel * 100) . '%', 'margin_of_error_percent' => ($marginOfError * 100) . '%', 'recommendation' => "Collect at least {$samplesPerVersion} predictions per version for statistically significant results", ]); } #[Route(path: '/api/ml/ab-test/select-version', method: Method::POST)] #[ApiEndpoint( summary: 'Select model version for traffic routing', description: 'Randomly select a model version based on A/B test traffic split configuration', tags: ['Machine Learning'], )] #[ApiRequestBody( description: 'Traffic routing configuration', required: true, example: [ 'model_name' => 'fraud-detector', 'version_a' => '1.0.0', 'version_b' => '2.0.0', 'traffic_split_a' => 0.8, ], )] #[ApiResponse( statusCode: 200, description: 'Version selected successfully', example: [ 'selected_version' => '2.0.0', 'model_name' => 'fraud-detector', 'traffic_split' => [ 'version_a' => 0.8, 'version_b' => 0.2, ], ], )] public function selectVersion(HttpRequest $request): JsonResult { try { $data = $request->parsedBody->toArray(); if (!isset($data['model_name'], $data['version_a'], $data['version_b'])) { return new JsonResult([ 'error' => 'Missing required fields', 'required' => ['model_name', 'version_a', 'version_b'], ], Status::BAD_REQUEST); } $versionA = Version::fromString($data['version_a']); $versionB = Version::fromString($data['version_b']); $config = new ABTestConfig( modelName: $data['model_name'], versionA: $versionA, versionB: $versionB, trafficSplitA: (float) ($data['traffic_split_a'] ?? 0.5), primaryMetric: 'accuracy' ); // Select version based on traffic split $selectedVersion = $this->abTesting->selectVersion($config); return new JsonResult([ 'selected_version' => $selectedVersion->toString(), 'model_name' => $config->modelName, 'traffic_split' => [ 'version_a' => $config->trafficSplitA, 'version_b' => 1.0 - $config->trafficSplitA, ], ]); } catch (\InvalidArgumentException $e) { return new JsonResult([ 'error' => 'Invalid routing configuration', 'message' => $e->getMessage(), ], Status::BAD_REQUEST); } } }