/** * Retrospective Validation Framework * * Provides tools for validating the treatment recommendation system against * real patient outcomes. Essential for: * - Model performance assessment * - Calibration checking * - Concordance analysis with actual treatments * - Outcome correlation studies * - Continuous quality improvement * * IMPORTANT: All patient data used for validation must be properly de-identified * or used with appropriate IRB approval and patient consent. */ import { EventEmitter } from 'events'; import { createHash } from 'crypto'; // ═══════════════════════════════════════════════════════════════════════════════ // VALIDATION DATA TYPES // ═══════════════════════════════════════════════════════════════════════════════ export interface ValidationPatient { id: string; // De-identified ID demographics: { ageAtDiagnosis: number; gender: 'male' | 'female'; ethnicity?: string; }; diagnosis: { cancerType: string; histology?: string; stage: string; diagnosisDate: Date; biomarkers?: { name: string; value: string | number; status?: string }[]; genomicAlterations?: { gene: string; alteration: string }[]; msiStatus?: 'MSI-H' | 'MSI-L' | 'MSS'; tmbValue?: number; pdl1Score?: number; hrdStatus?: boolean; }; treatment: { regimen: string; drugs: string[]; setting: string; startDate: Date; endDate?: Date; cycles?: number; doseModifications?: boolean; }; outcomes: { bestResponse?: 'CR' | 'PR' | 'SD' | 'PD' | 'NE'; responseDate?: Date; progressionDate?: Date; deathDate?: Date; lastFollowUpDate: Date; causeOfDeath?: 'disease' | 'treatment' | 'other' | 'unknown'; toxicities?: { name: string; grade: number; date?: Date }[]; }; ecogAtBaseline?: number; priorLines?: number; } export interface SystemRecommendation { patientId: string; recommendedRegimen: string; recommendedDrugs: string[]; predictions: { responseRate: number; pfsMonths: number; osMonths: number; toxicityRisk: number; }; matchingBiomarkers: string[]; confidenceScore: number; timestamp: Date; } export interface ValidationResult { patientId: string; concordance: { regimenMatch: boolean; partialMatch: boolean; // At least one drug matches matchedDrugs: string[]; }; outcomeComparison: { predictedResponse: number; actualResponse?: 'CR' | 'PR' | 'SD' | 'PD' | 'NE'; responseCorrect?: boolean; predictedPFS: number; actualPFS?: number; // months pfsDifference?: number; predictedOS: number; actualOS?: number; // months osDifference?: number; predictedToxicityRisk: number; actualGrade3PlusToxicity: boolean; }; clinicalBenefit: { objectiveResponse: boolean; diseaseControl: boolean; durableBenefit: boolean; // PFS > 6 months }; } export interface CohortAnalysis { cohortId: string; description: string; patientCount: number; dateRange: { start: Date; end: Date }; // Demographics demographics: { medianAge: number; ageRange: [number, number]; genderDistribution: { male: number; female: number }; stageDistribution: Record; }; // Concordance metrics concordance: { fullConcordance: number; // Percentage partialConcordance: number; noConcordance: number; concordanceByBiomarker: Record; }; // Prediction accuracy predictionAccuracy: { responseAccuracy: number; responseSensitivity: number; // Ability to predict responders responseSpecificity: number; // Ability to predict non-responders responseAUC?: number; pfsCIndex: number; // Concordance index for PFS pfsCalibration: number; // How well predicted PFS matches actual osCIndex: number; osCalibration: number; toxicityAccuracy: number; toxicityAUC?: number; }; // Clinical impact clinicalImpact: { objectiveResponseRate: number; diseaseControlRate: number; medianPFS: number; medianOS: number; // Comparison metrics concordantVsDiscordant: { concordantORR: number; discordantORR: number; concordantMedianPFS: number; discordantMedianPFS: number; pValue?: number; }; }; // Subgroup analyses subgroupAnalyses: { subgroup: string; patientCount: number; concordance: number; responseAccuracy: number; medianPFS: number; }[]; } // ═══════════════════════════════════════════════════════════════════════════════ // VALIDATION SERVICE // ═══════════════════════════════════════════════════════════════════════════════ export class RetrospectiveValidationService extends EventEmitter { private validationCohorts: Map = new Map(); private systemRecommendations: Map = new Map(); private validationResults: Map = new Map(); constructor() { super(); } /** * Load a validation cohort */ loadCohort(cohortId: string, patients: ValidationPatient[]): void { // De-identify and validate data const validatedPatients = patients.map(p => this.validateAndDeidentify(p)); this.validationCohorts.set(cohortId, validatedPatients); this.emit('cohort-loaded', { cohortId, patientCount: validatedPatients.length }); } /** * Load system recommendations for comparison */ loadRecommendations(recommendations: SystemRecommendation[]): void { for (const rec of recommendations) { this.systemRecommendations.set(rec.patientId, rec); } this.emit('recommendations-loaded', { count: recommendations.length }); } /** * Run validation for a cohort */ async runValidation(cohortId: string): Promise { const patients = this.validationCohorts.get(cohortId); if (!patients) { throw new Error(`Cohort ${cohortId} not found`); } const results: ValidationResult[] = []; // Validate each patient for (const patient of patients) { const recommendation = this.systemRecommendations.get(patient.id); const result = this.validatePatient(patient, recommendation); results.push(result); this.validationResults.set(patient.id, result); } // Compute cohort-level metrics const analysis = this.computeCohortAnalysis(cohortId, patients, results); this.emit('validation-complete', { cohortId, analysis }); return analysis; } /** * Validate a single patient */ private validatePatient( patient: ValidationPatient, recommendation?: SystemRecommendation ): ValidationResult { // Concordance analysis const concordance = this.assessConcordance(patient, recommendation); // Calculate actual outcomes const actualPFS = this.calculatePFS(patient); const actualOS = this.calculateOS(patient); const actualGrade3Plus = (patient.outcomes.toxicities || []).some(t => t.grade >= 3); // Compare predictions to outcomes const outcomeComparison = { predictedResponse: recommendation?.predictions.responseRate || 0, actualResponse: patient.outcomes.bestResponse, responseCorrect: this.assessResponseAccuracy( recommendation?.predictions.responseRate || 0, patient.outcomes.bestResponse ), predictedPFS: recommendation?.predictions.pfsMonths || 0, actualPFS, pfsDifference: actualPFS !== undefined ? actualPFS - (recommendation?.predictions.pfsMonths || 0) : undefined, predictedOS: recommendation?.predictions.osMonths || 0, actualOS, osDifference: actualOS !== undefined ? actualOS - (recommendation?.predictions.osMonths || 0) : undefined, predictedToxicityRisk: recommendation?.predictions.toxicityRisk || 0, actualGrade3PlusToxicity: actualGrade3Plus }; // Clinical benefit assessment const clinicalBenefit = { objectiveResponse: ['CR', 'PR'].includes(patient.outcomes.bestResponse || ''), diseaseControl: ['CR', 'PR', 'SD'].includes(patient.outcomes.bestResponse || ''), durableBenefit: actualPFS !== undefined && actualPFS >= 6 }; return { patientId: patient.id, concordance, outcomeComparison, clinicalBenefit }; } /** * Assess concordance between recommendation and actual treatment */ private assessConcordance( patient: ValidationPatient, recommendation?: SystemRecommendation ): ValidationResult['concordance'] { if (!recommendation) { return { regimenMatch: false, partialMatch: false, matchedDrugs: [] }; } const actualDrugs = patient.treatment.drugs.map(d => d.toLowerCase()); const recommendedDrugs = recommendation.recommendedDrugs.map(d => d.toLowerCase()); const matchedDrugs = recommendedDrugs.filter(rd => actualDrugs.some(ad => this.drugsMatch(ad, rd)) ); const regimenMatch = matchedDrugs.length === recommendedDrugs.length && matchedDrugs.length === actualDrugs.length; const partialMatch = matchedDrugs.length > 0; return { regimenMatch, partialMatch, matchedDrugs }; } /** * Check if two drug names refer to the same drug */ private drugsMatch(drug1: string, drug2: string): boolean { // Normalize names const normalize = (s: string) => s.toLowerCase().replace(/[^a-z0-9]/g, ''); if (normalize(drug1) === normalize(drug2)) return true; // Check brand/generic equivalents const equivalents: Record = { 'pembrolizumab': ['keytruda'], 'nivolumab': ['opdivo'], 'osimertinib': ['tagrisso'], 'alectinib': ['alecensa'], 'trastuzumab': ['herceptin'], 'pertuzumab': ['perjeta'], 'olaparib': ['lynparza'], 'palbociclib': ['ibrance'], 'carboplatin': ['paraplatin'], 'paclitaxel': ['taxol'], 'docetaxel': ['taxotere'] }; for (const [generic, brands] of Object.entries(equivalents)) { const allNames = [generic, ...brands].map(normalize); if (allNames.includes(normalize(drug1)) && allNames.includes(normalize(drug2))) { return true; } } return false; } /** * Calculate PFS in months */ private calculatePFS(patient: ValidationPatient): number | undefined { const startDate = patient.treatment.startDate; let endDate: Date; if (patient.outcomes.progressionDate) { endDate = patient.outcomes.progressionDate; } else if (patient.outcomes.deathDate) { endDate = patient.outcomes.deathDate; } else { // Censored at last follow-up endDate = patient.outcomes.lastFollowUpDate; } const months = (endDate.getTime() - startDate.getTime()) / (1000 * 60 * 60 * 24 * 30.44); return Math.round(months * 10) / 10; } /** * Calculate OS in months */ private calculateOS(patient: ValidationPatient): number | undefined { const startDate = patient.diagnosis.diagnosisDate; let endDate: Date; if (patient.outcomes.deathDate) { endDate = patient.outcomes.deathDate; } else { // Censored at last follow-up endDate = patient.outcomes.lastFollowUpDate; } const months = (endDate.getTime() - startDate.getTime()) / (1000 * 60 * 60 * 24 * 30.44); return Math.round(months * 10) / 10; } /** * Assess if response prediction was accurate */ private assessResponseAccuracy(predictedRate: number, actualResponse?: string): boolean | undefined { if (!actualResponse || actualResponse === 'NE') return undefined; const predicted = predictedRate >= 0.5 ? 'responder' : 'non-responder'; const actual = ['CR', 'PR'].includes(actualResponse) ? 'responder' : 'non-responder'; return predicted === actual; } /** * Compute cohort-level analysis */ private computeCohortAnalysis( cohortId: string, patients: ValidationPatient[], results: ValidationResult[] ): CohortAnalysis { // Demographics const ages = patients.map(p => p.demographics.ageAtDiagnosis); const maleCount = patients.filter(p => p.demographics.gender === 'male').length; const stageDistribution: Record = {}; for (const p of patients) { stageDistribution[p.diagnosis.stage] = (stageDistribution[p.diagnosis.stage] || 0) + 1; } // Concordance metrics const fullConcordance = results.filter(r => r.concordance.regimenMatch).length / results.length; const partialConcordance = results.filter(r => r.concordance.partialMatch && !r.concordance.regimenMatch).length / results.length; // Response accuracy const responseResults = results.filter(r => r.outcomeComparison.responseCorrect !== undefined); const responseAccuracy = responseResults.length > 0 ? responseResults.filter(r => r.outcomeComparison.responseCorrect).length / responseResults.length : 0; // Sensitivity (correctly predicted responders) const actualResponders = results.filter(r => ['CR', 'PR'].includes(r.outcomeComparison.actualResponse || '') ); const responseSensitivity = actualResponders.length > 0 ? actualResponders.filter(r => r.outcomeComparison.predictedResponse >= 0.5).length / actualResponders.length : 0; // Specificity (correctly predicted non-responders) const actualNonResponders = results.filter(r => ['SD', 'PD'].includes(r.outcomeComparison.actualResponse || '') ); const responseSpecificity = actualNonResponders.length > 0 ? actualNonResponders.filter(r => r.outcomeComparison.predictedResponse < 0.5).length / actualNonResponders.length : 0; // PFS concordance index (simplified) const pfsCIndex = this.calculateCIndex( results.map(r => r.outcomeComparison.predictedPFS), results.map(r => r.outcomeComparison.actualPFS || 0) ); // PFS calibration const pfsCalibration = this.calculateCalibration( results.map(r => r.outcomeComparison.predictedPFS), results.map(r => r.outcomeComparison.actualPFS || 0) ); // OS metrics (similar calculation) const osCIndex = this.calculateCIndex( results.map(r => r.outcomeComparison.predictedOS), results.map(r => r.outcomeComparison.actualOS || 0) ); const osCalibration = this.calculateCalibration( results.map(r => r.outcomeComparison.predictedOS), results.map(r => r.outcomeComparison.actualOS || 0) ); // Toxicity accuracy const toxicityResults = results.filter(r => r.outcomeComparison.predictedToxicityRisk > 0); const toxicityAccuracy = toxicityResults.length > 0 ? toxicityResults.filter(r => { const predicted = r.outcomeComparison.predictedToxicityRisk >= 0.3; return predicted === r.outcomeComparison.actualGrade3PlusToxicity; }).length / toxicityResults.length : 0; // Clinical outcomes const objectiveResponseRate = results.filter(r => r.clinicalBenefit.objectiveResponse).length / results.length; const diseaseControlRate = results.filter(r => r.clinicalBenefit.diseaseControl).length / results.length; const pfsValues = results.map(r => r.outcomeComparison.actualPFS).filter(v => v !== undefined) as number[]; const medianPFS = this.calculateMedian(pfsValues); const osValues = results.map(r => r.outcomeComparison.actualOS).filter(v => v !== undefined) as number[]; const medianOS = this.calculateMedian(osValues); // Concordant vs discordant outcomes const concordantResults = results.filter(r => r.concordance.regimenMatch || r.concordance.partialMatch); const discordantResults = results.filter(r => !r.concordance.regimenMatch && !r.concordance.partialMatch); const concordantORR = concordantResults.length > 0 ? concordantResults.filter(r => r.clinicalBenefit.objectiveResponse).length / concordantResults.length : 0; const discordantORR = discordantResults.length > 0 ? discordantResults.filter(r => r.clinicalBenefit.objectiveResponse).length / discordantResults.length : 0; const concordantPFS = this.calculateMedian( concordantResults.map(r => r.outcomeComparison.actualPFS).filter(v => v !== undefined) as number[] ); const discordantPFS = this.calculateMedian( discordantResults.map(r => r.outcomeComparison.actualPFS).filter(v => v !== undefined) as number[] ); // Subgroup analyses const subgroupAnalyses = this.performSubgroupAnalyses(patients, results); return { cohortId, description: `Validation cohort with ${patients.length} patients`, patientCount: patients.length, dateRange: { start: new Date(Math.min(...patients.map(p => p.diagnosis.diagnosisDate.getTime()))), end: new Date(Math.max(...patients.map(p => p.outcomes.lastFollowUpDate.getTime()))) }, demographics: { medianAge: this.calculateMedian(ages), ageRange: [Math.min(...ages), Math.max(...ages)], genderDistribution: { male: maleCount / patients.length, female: (patients.length - maleCount) / patients.length }, stageDistribution }, concordance: { fullConcordance, partialConcordance, noConcordance: 1 - fullConcordance - partialConcordance, concordanceByBiomarker: this.calculateConcordanceByBiomarker(patients, results) }, predictionAccuracy: { responseAccuracy, responseSensitivity, responseSpecificity, pfsCIndex, pfsCalibration, osCIndex, osCalibration, toxicityAccuracy }, clinicalImpact: { objectiveResponseRate, diseaseControlRate, medianPFS, medianOS, concordantVsDiscordant: { concordantORR, discordantORR, concordantMedianPFS: concordantPFS, discordantMedianPFS: discordantPFS } }, subgroupAnalyses }; } /** * Calculate concordance index (C-index) */ private calculateCIndex(predicted: number[], actual: number[]): number { let concordant = 0; let discordant = 0; let tied = 0; for (let i = 0; i < predicted.length; i++) { for (let j = i + 1; j < predicted.length; j++) { if (actual[i] !== actual[j]) { if ((predicted[i] > predicted[j] && actual[i] > actual[j]) || (predicted[i] < predicted[j] && actual[i] < actual[j])) { concordant++; } else if (predicted[i] !== predicted[j]) { discordant++; } else { tied++; } } } } const total = concordant + discordant + tied; return total > 0 ? (concordant + 0.5 * tied) / total : 0.5; } /** * Calculate calibration score (0-1, higher is better) */ private calculateCalibration(predicted: number[], actual: number[]): number { if (predicted.length === 0) return 0; // Group predictions into deciles and compare to actual const combined = predicted.map((p, i) => ({ predicted: p, actual: actual[i] })) .filter(c => c.actual > 0) .sort((a, b) => a.predicted - b.predicted); if (combined.length < 10) { // Simple correlation for small samples const meanPred = combined.reduce((s, c) => s + c.predicted, 0) / combined.length; const meanActual = combined.reduce((s, c) => s + c.actual, 0) / combined.length; const errors = combined.map(c => Math.abs(c.predicted - c.actual) / Math.max(c.actual, 1)); const meanError = errors.reduce((s, e) => s + e, 0) / errors.length; return Math.max(0, 1 - meanError); } // For larger samples, use decile calibration const decileSize = Math.ceil(combined.length / 10); let totalError = 0; for (let i = 0; i < 10; i++) { const start = i * decileSize; const end = Math.min((i + 1) * decileSize, combined.length); const decile = combined.slice(start, end); const avgPredicted = decile.reduce((s, c) => s + c.predicted, 0) / decile.length; const avgActual = decile.reduce((s, c) => s + c.actual, 0) / decile.length; const relativeError = avgActual > 0 ? Math.abs(avgPredicted - avgActual) / avgActual : 0; totalError += relativeError; } return Math.max(0, 1 - totalError / 10); } /** * Calculate median of an array */ private calculateMedian(values: number[]): number { if (values.length === 0) return 0; const sorted = [...values].sort((a, b) => a - b); const mid = Math.floor(sorted.length / 2); return sorted.length % 2 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2; } /** * Calculate concordance by biomarker subgroup */ private calculateConcordanceByBiomarker( patients: ValidationPatient[], results: ValidationResult[] ): Record { const biomarkerGroups: Record = {}; for (let i = 0; i < patients.length; i++) { const patient = patients[i]; const result = results[i]; // Check various biomarker categories const biomarkers: string[] = []; if (patient.diagnosis.msiStatus === 'MSI-H') biomarkers.push('MSI-H'); if (patient.diagnosis.tmbValue && patient.diagnosis.tmbValue >= 10) biomarkers.push('TMB-H'); if (patient.diagnosis.pdl1Score && patient.diagnosis.pdl1Score >= 50) biomarkers.push('PD-L1≥50%'); if (patient.diagnosis.hrdStatus) biomarkers.push('HRD+'); // Check genomic alterations for (const alt of patient.diagnosis.genomicAlterations || []) { biomarkers.push(`${alt.gene} ${alt.alteration}`); } // Record concordance for each biomarker for (const biomarker of biomarkers) { if (!biomarkerGroups[biomarker]) { biomarkerGroups[biomarker] = { concordant: 0, total: 0 }; } biomarkerGroups[biomarker].total++; if (result.concordance.regimenMatch || result.concordance.partialMatch) { biomarkerGroups[biomarker].concordant++; } } } // Convert to concordance rates const concordanceByBiomarker: Record = {}; for (const [biomarker, data] of Object.entries(biomarkerGroups)) { if (data.total >= 5) { // Only include groups with meaningful sample size concordanceByBiomarker[biomarker] = data.concordant / data.total; } } return concordanceByBiomarker; } /** * Perform subgroup analyses */ private performSubgroupAnalyses( patients: ValidationPatient[], results: ValidationResult[] ): CohortAnalysis['subgroupAnalyses'] { const subgroups: CohortAnalysis['subgroupAnalyses'] = []; // By cancer type const cancerTypes = [...new Set(patients.map(p => p.diagnosis.cancerType))]; for (const cancerType of cancerTypes) { const indices = patients.map((p, i) => p.diagnosis.cancerType === cancerType ? i : -1).filter(i => i >= 0); if (indices.length >= 10) { const subgroupResults = indices.map(i => results[i]); const subgroupPatients = indices.map(i => patients[i]); subgroups.push({ subgroup: cancerType, patientCount: indices.length, concordance: subgroupResults.filter(r => r.concordance.regimenMatch || r.concordance.partialMatch).length / indices.length, responseAccuracy: this.calculateSubgroupResponseAccuracy(subgroupResults), medianPFS: this.calculateMedian( subgroupResults.map(r => r.outcomeComparison.actualPFS).filter(v => v !== undefined) as number[] ) }); } } // By stage const stages = ['I', 'II', 'III', 'IV']; for (const stageGroup of stages) { const indices = patients.map((p, i) => p.diagnosis.stage.startsWith(stageGroup) ? i : -1).filter(i => i >= 0); if (indices.length >= 10) { const subgroupResults = indices.map(i => results[i]); subgroups.push({ subgroup: `Stage ${stageGroup}`, patientCount: indices.length, concordance: subgroupResults.filter(r => r.concordance.regimenMatch || r.concordance.partialMatch).length / indices.length, responseAccuracy: this.calculateSubgroupResponseAccuracy(subgroupResults), medianPFS: this.calculateMedian( subgroupResults.map(r => r.outcomeComparison.actualPFS).filter(v => v !== undefined) as number[] ) }); } } // By age group const ageGroups = [ { name: 'Age <50', filter: (p: ValidationPatient) => p.demographics.ageAtDiagnosis < 50 }, { name: 'Age 50-65', filter: (p: ValidationPatient) => p.demographics.ageAtDiagnosis >= 50 && p.demographics.ageAtDiagnosis < 65 }, { name: 'Age 65-75', filter: (p: ValidationPatient) => p.demographics.ageAtDiagnosis >= 65 && p.demographics.ageAtDiagnosis < 75 }, { name: 'Age ≥75', filter: (p: ValidationPatient) => p.demographics.ageAtDiagnosis >= 75 } ]; for (const ageGroup of ageGroups) { const indices = patients.map((p, i) => ageGroup.filter(p) ? i : -1).filter(i => i >= 0); if (indices.length >= 10) { const subgroupResults = indices.map(i => results[i]); subgroups.push({ subgroup: ageGroup.name, patientCount: indices.length, concordance: subgroupResults.filter(r => r.concordance.regimenMatch || r.concordance.partialMatch).length / indices.length, responseAccuracy: this.calculateSubgroupResponseAccuracy(subgroupResults), medianPFS: this.calculateMedian( subgroupResults.map(r => r.outcomeComparison.actualPFS).filter(v => v !== undefined) as number[] ) }); } } return subgroups; } /** * Calculate response accuracy for a subgroup */ private calculateSubgroupResponseAccuracy(results: ValidationResult[]): number { const withResponse = results.filter(r => r.outcomeComparison.responseCorrect !== undefined); return withResponse.length > 0 ? withResponse.filter(r => r.outcomeComparison.responseCorrect).length / withResponse.length : 0; } /** * Validate and de-identify patient data */ private validateAndDeidentify(patient: ValidationPatient): ValidationPatient { // Generate hashed de-identified ID const deidentifiedId = createHash('sha256') .update(patient.id + 'SALT_CHANGE_IN_PRODUCTION') .digest('hex') .substring(0, 12); return { ...patient, id: deidentifiedId }; } /** * Generate validation report */ generateReport(analysis: CohortAnalysis): string { const lines: string[] = [ '═'.repeat(70), ' RETROSPECTIVE VALIDATION REPORT', '═'.repeat(70), '', `Cohort: ${analysis.cohortId}`, `Patients: ${analysis.patientCount}`, `Date Range: ${analysis.dateRange.start.toISOString().split('T')[0]} to ${analysis.dateRange.end.toISOString().split('T')[0]}`, '', '─'.repeat(70), ' CONCORDANCE METRICS', '─'.repeat(70), `Full Concordance: ${(analysis.concordance.fullConcordance * 100).toFixed(1)}%`, `Partial Concordance: ${(analysis.concordance.partialConcordance * 100).toFixed(1)}%`, `No Concordance: ${(analysis.concordance.noConcordance * 100).toFixed(1)}%`, '', '─'.repeat(70), ' PREDICTION ACCURACY', '─'.repeat(70), `Response Accuracy: ${(analysis.predictionAccuracy.responseAccuracy * 100).toFixed(1)}%`, `Response Sensitivity: ${(analysis.predictionAccuracy.responseSensitivity * 100).toFixed(1)}%`, `Response Specificity: ${(analysis.predictionAccuracy.responseSpecificity * 100).toFixed(1)}%`, `PFS C-Index: ${analysis.predictionAccuracy.pfsCIndex.toFixed(3)}`, `PFS Calibration: ${(analysis.predictionAccuracy.pfsCalibration * 100).toFixed(1)}%`, `OS C-Index: ${analysis.predictionAccuracy.osCIndex.toFixed(3)}`, `Toxicity Accuracy: ${(analysis.predictionAccuracy.toxicityAccuracy * 100).toFixed(1)}%`, '', '─'.repeat(70), ' CLINICAL OUTCOMES', '─'.repeat(70), `Objective Response Rate: ${(analysis.clinicalImpact.objectiveResponseRate * 100).toFixed(1)}%`, `Disease Control Rate: ${(analysis.clinicalImpact.diseaseControlRate * 100).toFixed(1)}%`, `Median PFS: ${analysis.clinicalImpact.medianPFS.toFixed(1)} months`, `Median OS: ${analysis.clinicalImpact.medianOS.toFixed(1)} months`, '', ' Concordant vs Discordant Treatment:', ` Concordant ORR: ${(analysis.clinicalImpact.concordantVsDiscordant.concordantORR * 100).toFixed(1)}%`, ` Discordant ORR: ${(analysis.clinicalImpact.concordantVsDiscordant.discordantORR * 100).toFixed(1)}%`, ` Concordant Median PFS: ${analysis.clinicalImpact.concordantVsDiscordant.concordantMedianPFS.toFixed(1)} months`, ` Discordant Median PFS: ${analysis.clinicalImpact.concordantVsDiscordant.discordantMedianPFS.toFixed(1)} months`, '', '─'.repeat(70), ' SUBGROUP ANALYSES', '─'.repeat(70), ...analysis.subgroupAnalyses.map(sg => `${sg.subgroup}: n=${sg.patientCount}, Concordance=${(sg.concordance * 100).toFixed(0)}%, ` + `Accuracy=${(sg.responseAccuracy * 100).toFixed(0)}%, mPFS=${sg.medianPFS.toFixed(1)}mo` ), '', '═'.repeat(70) ]; return lines.join('\n'); } /** * Export validation results */ exportResults(cohortId: string, format: 'json' | 'csv'): string { const patients = this.validationCohorts.get(cohortId); if (!patients) return ''; const results: any[] = []; for (const patient of patients) { const result = this.validationResults.get(patient.id); if (result) { results.push({ patientId: patient.id, cancerType: patient.diagnosis.cancerType, stage: patient.diagnosis.stage, actualRegimen: patient.treatment.regimen, regimenMatch: result.concordance.regimenMatch, partialMatch: result.concordance.partialMatch, predictedResponse: result.outcomeComparison.predictedResponse, actualResponse: result.outcomeComparison.actualResponse, responseCorrect: result.outcomeComparison.responseCorrect, predictedPFS: result.outcomeComparison.predictedPFS, actualPFS: result.outcomeComparison.actualPFS, predictedOS: result.outcomeComparison.predictedOS, actualOS: result.outcomeComparison.actualOS, objectiveResponse: result.clinicalBenefit.objectiveResponse, diseaseControl: result.clinicalBenefit.diseaseControl }); } } if (format === 'json') { return JSON.stringify(results, null, 2); } // CSV format const headers = Object.keys(results[0] || {}); const rows = [ headers.join(','), ...results.map(r => headers.map(h => JSON.stringify(r[h] ?? '')).join(',')) ]; return rows.join('\n'); } } export default RetrospectiveValidationService;